From e795375559f8a8f3b936ae87c0c4c803b0643096 Mon Sep 17 00:00:00 2001 From: Mike Bestcat Date: Sun, 7 Jun 2026 14:02:42 +0300 Subject: [PATCH 01/21] Scaffold for sharing --- .../api/publish/__init__.py | 1 + .../api/publish/publish_entity_activate.py | 206 ++++++++++++++ .../api/publish/publish_entity_create.py | 212 +++++++++++++++ .../publish_entity_load_by_external_id.py | 212 +++++++++++++++ .../api/publish/publish_entity_to_draft.py | 206 ++++++++++++++ .../jupiter_webapi_client/models/__init__.py | 22 +- .../models/app_component.py | 20 -- .../models/google_oauth_redirect_state.py | 78 ++++++ .../models/publish_domain.py | 144 ++++++++++ .../models/publish_entity.py | 185 +++++++++++++ .../models/publish_entity_activate_args.py | 62 +++++ .../models/publish_entity_create_args.py | 78 ++++++ .../models/publish_entity_create_result.py | 68 +++++ ...publish_entity_load_by_external_id_args.py | 62 +++++ ...blish_entity_load_by_external_id_result.py | 68 +++++ .../models/publish_entity_status.py | 9 + .../models/publish_entity_to_draft_args.py | 62 +++++ gen/ts/webapi-client/gen/ApiClient.ts | 3 + gen/ts/webapi-client/gen/index.ts | 15 +- .../webapi-client/gen/models/AppComponent.ts | 16 +- .../gen/models/GoogleOauthRedirectState.ts | 14 + .../webapi-client/gen/models/PublishDomain.ts | 20 ++ .../webapi-client/gen/models/PublishEntity.ts | 28 ++ .../gen/models/PublishEntityActivateArgs.ts | 12 + .../gen/models/PublishEntityCreateArgs.ts | 15 ++ .../gen/models/PublishEntityCreateResult.ts | 12 + .../PublishEntityLoadByExternalIdArgs.ts | 12 + .../PublishEntityLoadByExternalIdResult.ts | 12 + .../gen/models/PublishEntityName.ts | 8 + .../gen/models/PublishEntityStatus.ts | 11 + .../gen/models/PublishEntityToDraftArgs.ts | 12 + .../gen/models/PublishExternalId.ts | 8 + .../gen/services/ApiKeyService.ts | 12 +- .../gen/services/ApplicationService.ts | 20 +- .../webapi-client/gen/services/AuthService.ts | 10 +- .../gen/services/BigPlansService.ts | 26 +- .../gen/services/CalendarService.ts | 2 +- .../gen/services/ChoresService.ts | 18 +- .../gen/services/ContactsService.ts | 14 +- .../webapi-client/gen/services/DocsService.ts | 24 +- .../webapi-client/gen/services/GcService.ts | 4 +- .../webapi-client/gen/services/GenService.ts | 4 +- .../gen/services/HabitsService.ts | 18 +- .../webapi-client/gen/services/HomeService.ts | 24 +- .../gen/services/InboxTasksService.ts | 10 +- .../gen/services/InfraService.ts | 6 +- .../gen/services/JournalsService.ts | 22 +- .../gen/services/LifePlanService.ts | 72 ++--- .../gen/services/McpKeyService.ts | 12 +- .../gen/services/MetricsService.ts | 26 +- .../webapi-client/gen/services/MotdService.ts | 2 +- .../gen/services/NotesService.ts | 14 +- .../webapi-client/gen/services/PrmService.ts | 38 +-- .../gen/services/PublishService.ts | 127 +++++++++ .../gen/services/PushIntegrationsService.ts | 24 +- .../gen/services/ReportService.ts | 2 +- .../gen/services/ScheduleService.ts | 56 ++-- .../gen/services/SearchService.ts | 2 +- .../gen/services/SmartListsService.ts | 22 +- .../gen/services/StatsService.ts | 4 +- .../webapi-client/gen/services/TagsService.ts | 14 +- .../gen/services/TestHelperService.ts | 10 +- .../gen/services/TimeEventsService.ts | 20 +- .../gen/services/TimePlansService.ts | 42 +-- .../webapi-client/gen/services/TodoService.ts | 12 +- .../gen/services/UsersService.ts | 10 +- .../gen/services/VacationsService.ts | 12 +- .../gen/services/WorkingMemService.ts | 6 +- .../gen/services/WorkspacesService.ts | 6 +- itests/api/common/publish.test.py | 252 ++++++++++++++++++ src/api/jupiter/api/jupiter.py | 39 +++ src/cli/jupiter/cli/command/exceptions.py | 24 ++ .../jupiter/core/application/use_case/init.py | 9 + .../use_case/init_create_workspace.py | 9 + .../core/common/sub/publish/__init__.py | 3 + .../core/common/sub/publish/impl/__init__.py | 1 + .../sub/publish/impl/postgres_repository.py | 53 ++++ .../sub/publish/impl/sqlite_repository.py | 53 ++++ .../jupiter/core/common/sub/publish/root.py | 29 ++ .../core/common/sub/publish/sub/__init__.py | 1 + .../common/sub/publish/sub/entity/__init__.py | 1 + .../sub/publish/sub/entity/external_id.py | 54 ++++ .../publish/sub/entity/external_id.test.py | 24 ++ .../common/sub/publish/sub/entity/name.py | 9 + .../common/sub/publish/sub/entity/root.py | 129 +++++++++ .../sub/publish/sub/entity/root.test.py | 39 +++ .../common/sub/publish/sub/entity/status.py | 11 + .../publish/sub/entity/use_case/__init__.py | 1 + .../publish/sub/entity/use_case/activate.py | 50 ++++ .../sub/publish/sub/entity/use_case/create.py | 67 +++++ .../entity/use_case/load_by_external_id.py | 59 ++++ .../publish/sub/entity/use_case/to_draft.py | 50 ++++ src/core/jupiter/core/workspaces/root.py | 2 + .../2026_06_07_00_29_publish_domain.py | 119 +++++++++ .../2026_06_07_00_29_publish_domain.py | 119 +++++++++ src/mcp/jupiter/mcp/jupiter.py | 35 +++ .../exceptions.py | 14 + .../exceptions.py | 14 + .../jupiter_webapi_gc_do_all/exceptions.py | 14 + .../jupiter_webapi_gen_do_all/exceptions.py | 14 + .../exceptions.py | 14 + .../exceptions.py | 14 + .../exceptions.py | 14 + .../exceptions.py | 14 + src/webapi/srv/jupiter/webapi/exceptions.py | 42 +++ .../jupiter_webapi_stats_do_all/exceptions.py | 14 + .../exceptions.py | 14 + 107 files changed, 3716 insertions(+), 348 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/publish/__init__.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_activate.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_create.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load_by_external_id.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_to_draft.py delete mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/app_component.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/google_oauth_redirect_state.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_domain.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_activate_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_result.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_by_external_id_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_by_external_id_result.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_status.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_to_draft_args.py create mode 100644 gen/ts/webapi-client/gen/models/GoogleOauthRedirectState.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishDomain.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntity.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityActivateArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityCreateArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityCreateResult.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityLoadByExternalIdArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityLoadByExternalIdResult.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityName.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityStatus.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityToDraftArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishExternalId.ts create mode 100644 gen/ts/webapi-client/gen/services/PublishService.ts create mode 100644 itests/api/common/publish.test.py create mode 100644 src/core/jupiter/core/common/sub/publish/__init__.py create mode 100644 src/core/jupiter/core/common/sub/publish/impl/__init__.py create mode 100644 src/core/jupiter/core/common/sub/publish/impl/postgres_repository.py create mode 100644 src/core/jupiter/core/common/sub/publish/impl/sqlite_repository.py create mode 100644 src/core/jupiter/core/common/sub/publish/root.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/__init__.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/__init__.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/external_id.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/external_id.test.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/name.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/root.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/root.test.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/status.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/use_case/__init__.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/use_case/activate.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/use_case/create.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load_by_external_id.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/use_case/to_draft.py create mode 100644 src/core/migrations/postgres/versions/2026_06_07_00_29_publish_domain.py create mode 100644 src/core/migrations/sqlite/versions/2026_06_07_00_29_publish_domain.py diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/publish/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/api/publish/__init__.py new file mode 100644 index 000000000..2d7c0b23d --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/publish/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_activate.py b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_activate.py new file mode 100644 index 000000000..a8e16f8fa --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_activate.py @@ -0,0 +1,206 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.publish_entity_activate_args import PublishEntityActivateArgs +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: PublishEntityActivateArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/publish-entity-activate", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: + if response.status_code == 200: + response_200 = cast(Any, None) + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityActivateArgs | Unset = UNSET, +) -> Response[Any | ErrorResponse]: + """Use case for activating a publish entity. + + Args: + body (PublishEntityActivateArgs | Unset): PublishEntityActivate args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: PublishEntityActivateArgs | Unset = UNSET, +) -> Any | ErrorResponse | None: + """Use case for activating a publish entity. + + Args: + body (PublishEntityActivateArgs | Unset): PublishEntityActivate args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | ErrorResponse + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityActivateArgs | Unset = UNSET, +) -> Response[Any | ErrorResponse]: + """Use case for activating a publish entity. + + Args: + body (PublishEntityActivateArgs | Unset): PublishEntityActivate args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: PublishEntityActivateArgs | Unset = UNSET, +) -> Any | ErrorResponse | None: + """Use case for activating a publish entity. + + Args: + body (PublishEntityActivateArgs | Unset): PublishEntityActivate args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | ErrorResponse + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_create.py b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_create.py new file mode 100644 index 000000000..f872583aa --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_create.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.publish_entity_create_args import PublishEntityCreateArgs +from ...models.publish_entity_create_result import PublishEntityCreateResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: PublishEntityCreateArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/publish-entity-create", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | PublishEntityCreateResult | None: + if response.status_code == 200: + response_200 = PublishEntityCreateResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | PublishEntityCreateResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityCreateArgs | Unset = UNSET, +) -> Response[ErrorResponse | PublishEntityCreateResult]: + """Use case for creating a publish entity. + + Args: + body (PublishEntityCreateArgs | Unset): PublishEntityCreate args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PublishEntityCreateResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: PublishEntityCreateArgs | Unset = UNSET, +) -> ErrorResponse | PublishEntityCreateResult | None: + """Use case for creating a publish entity. + + Args: + body (PublishEntityCreateArgs | Unset): PublishEntityCreate args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PublishEntityCreateResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityCreateArgs | Unset = UNSET, +) -> Response[ErrorResponse | PublishEntityCreateResult]: + """Use case for creating a publish entity. + + Args: + body (PublishEntityCreateArgs | Unset): PublishEntityCreate args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PublishEntityCreateResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: PublishEntityCreateArgs | Unset = UNSET, +) -> ErrorResponse | PublishEntityCreateResult | None: + """Use case for creating a publish entity. + + Args: + body (PublishEntityCreateArgs | Unset): PublishEntityCreate args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PublishEntityCreateResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load_by_external_id.py b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load_by_external_id.py new file mode 100644 index 000000000..602675abc --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load_by_external_id.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.publish_entity_load_by_external_id_args import PublishEntityLoadByExternalIdArgs +from ...models.publish_entity_load_by_external_id_result import PublishEntityLoadByExternalIdResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: PublishEntityLoadByExternalIdArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/publish-entity-load-by-external-id", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | PublishEntityLoadByExternalIdResult | None: + if response.status_code == 200: + response_200 = PublishEntityLoadByExternalIdResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | PublishEntityLoadByExternalIdResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: PublishEntityLoadByExternalIdArgs | Unset = UNSET, +) -> Response[ErrorResponse | PublishEntityLoadByExternalIdResult]: + """Load a publish entity by its external id. + + Args: + body (PublishEntityLoadByExternalIdArgs | Unset): PublishEntityLoadByExternalId args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PublishEntityLoadByExternalIdResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: PublishEntityLoadByExternalIdArgs | Unset = UNSET, +) -> ErrorResponse | PublishEntityLoadByExternalIdResult | None: + """Load a publish entity by its external id. + + Args: + body (PublishEntityLoadByExternalIdArgs | Unset): PublishEntityLoadByExternalId args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PublishEntityLoadByExternalIdResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: PublishEntityLoadByExternalIdArgs | Unset = UNSET, +) -> Response[ErrorResponse | PublishEntityLoadByExternalIdResult]: + """Load a publish entity by its external id. + + Args: + body (PublishEntityLoadByExternalIdArgs | Unset): PublishEntityLoadByExternalId args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PublishEntityLoadByExternalIdResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: PublishEntityLoadByExternalIdArgs | Unset = UNSET, +) -> ErrorResponse | PublishEntityLoadByExternalIdResult | None: + """Load a publish entity by its external id. + + Args: + body (PublishEntityLoadByExternalIdArgs | Unset): PublishEntityLoadByExternalId args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PublishEntityLoadByExternalIdResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_to_draft.py b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_to_draft.py new file mode 100644 index 000000000..ef04e3585 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_to_draft.py @@ -0,0 +1,206 @@ +from http import HTTPStatus +from typing import Any, cast + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.publish_entity_to_draft_args import PublishEntityToDraftArgs +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: PublishEntityToDraftArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/publish-entity-to-draft", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Any | ErrorResponse | None: + if response.status_code == 200: + response_200 = cast(Any, None) + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Any | ErrorResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityToDraftArgs | Unset = UNSET, +) -> Response[Any | ErrorResponse]: + """Use case for moving a publish entity back to draft. + + Args: + body (PublishEntityToDraftArgs | Unset): PublishEntityToDraft args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: PublishEntityToDraftArgs | Unset = UNSET, +) -> Any | ErrorResponse | None: + """Use case for moving a publish entity back to draft. + + Args: + body (PublishEntityToDraftArgs | Unset): PublishEntityToDraft args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | ErrorResponse + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityToDraftArgs | Unset = UNSET, +) -> Response[Any | ErrorResponse]: + """Use case for moving a publish entity back to draft. + + Args: + body (PublishEntityToDraftArgs | Unset): PublishEntityToDraft args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: PublishEntityToDraftArgs | Unset = UNSET, +) -> Any | ErrorResponse | None: + """Use case for moving a publish entity back to draft. + + Args: + body (PublishEntityToDraftArgs | Unset): PublishEntityToDraft args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Any | ErrorResponse + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index fbdcccdc9..014d267d8 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -30,7 +30,6 @@ from .api_key_summary import APIKeySummary from .api_key_update_args import APIKeyUpdateArgs from .api_key_update_args_name import APIKeyUpdateArgsName -from .app_component import AppComponent from .app_core import AppCore from .app_distribution import AppDistribution from .app_distribution_state import AppDistributionState @@ -297,6 +296,7 @@ from .goal_update_args_parent_goal_ref_id import GoalUpdateArgsParentGoalRefId from .google_id_token_claims import GoogleIdTokenClaims from .google_o_auth_token_response import GoogleOAuthTokenResponse +from .google_oauth_redirect_state import GoogleOauthRedirectState from .habit import Habit from .habit_archive_args import HabitArchiveArgs from .habit_collection import HabitCollection @@ -618,6 +618,15 @@ from .planned_time_and_effort_summary_hours_by_feasability import PlannedTimeAndEffortSummaryHoursByFeasability from .planned_time_and_effort_summary_score_by_feasability import PlannedTimeAndEffortSummaryScoreByFeasability from .prm import PRM +from .publish_domain import PublishDomain +from .publish_entity import PublishEntity +from .publish_entity_activate_args import PublishEntityActivateArgs +from .publish_entity_create_args import PublishEntityCreateArgs +from .publish_entity_create_result import PublishEntityCreateResult +from .publish_entity_load_by_external_id_args import PublishEntityLoadByExternalIdArgs +from .publish_entity_load_by_external_id_result import PublishEntityLoadByExternalIdResult +from .publish_entity_status import PublishEntityStatus +from .publish_entity_to_draft_args import PublishEntityToDraftArgs from .push_generation_extra_info import PushGenerationExtraInfo from .push_integration_group import PushIntegrationGroup from .quote_block import QuoteBlock @@ -1025,7 +1034,6 @@ "APIKeySummary", "APIKeyUpdateArgs", "APIKeyUpdateArgsName", - "AppComponent", "AppCore", "AppDistribution", "AppDistributionState", @@ -1291,6 +1299,7 @@ "GoalUpdateArgsName", "GoalUpdateArgsParentGoalRefId", "GoogleIdTokenClaims", + "GoogleOauthRedirectState", "GoogleOAuthTokenResponse", "Habit", "HabitArchiveArgs", @@ -1601,6 +1610,15 @@ "PlannedTimeAndEffortSummaryHoursByFeasability", "PlannedTimeAndEffortSummaryScoreByFeasability", "PRM", + "PublishDomain", + "PublishEntity", + "PublishEntityActivateArgs", + "PublishEntityCreateArgs", + "PublishEntityCreateResult", + "PublishEntityLoadByExternalIdArgs", + "PublishEntityLoadByExternalIdResult", + "PublishEntityStatus", + "PublishEntityToDraftArgs", "PushGenerationExtraInfo", "PushIntegrationGroup", "QuoteBlock", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/app_component.py b/gen/py/webapi-client/jupiter_webapi_client/models/app_component.py deleted file mode 100644 index c492d81f2..000000000 --- a/gen/py/webapi-client/jupiter_webapi_client/models/app_component.py +++ /dev/null @@ -1,20 +0,0 @@ -from enum import Enum - - -class AppComponent(str, Enum): - APP = "app" - CLEAR_ABANDONED_USERS_CRON = "clear-abandoned-users-cron" - CLI = "cli" - CRM_BACKFILL = "crm-backfill" - GC_CRON = "gc-cron" - GEN_CRON = "gen-cron" - SCHEDULE_EXTERNAL_SYNC_CRON = "schedule-external-sync-cron" - SEARCH_INDEX_BACKFILL = "search-index-backfill" - SEARCH_MUTATION_LOG_DRAIN = "search-mutation-log-drain" - SEARCH_MUTATION_REQUEUE = "search-mutation-requeue" - STATS_CRON = "stats-cron" - SYNC_GOOGLE_USER_DATA_DO_ALL = "sync-google-user-data-do-all" - WEB = "web" - - def __str__(self) -> str: - return str(self.value) diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/google_oauth_redirect_state.py b/gen/py/webapi-client/jupiter_webapi_client/models/google_oauth_redirect_state.py new file mode 100644 index 000000000..cc3a8e729 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/google_oauth_redirect_state.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="GoogleOauthRedirectState") + + +@_attrs_define +class GoogleOauthRedirectState: + """OAuth state embedding post-auth redirect targets. + + Attributes: + nonce (str): + callback_success_url (str): A system URL that may point at localhost. + callback_failure_url (str): A system URL that may point at localhost. + """ + + nonce: str + callback_success_url: str + callback_failure_url: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + nonce = self.nonce + + callback_success_url = self.callback_success_url + + callback_failure_url = self.callback_failure_url + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "nonce": nonce, + "callback_success_url": callback_success_url, + "callback_failure_url": callback_failure_url, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + nonce = d.pop("nonce") + + callback_success_url = d.pop("callback_success_url") + + callback_failure_url = d.pop("callback_failure_url") + + google_oauth_redirect_state = cls( + nonce=nonce, + callback_success_url=callback_success_url, + callback_failure_url=callback_failure_url, + ) + + google_oauth_redirect_state.additional_properties = d + return google_oauth_redirect_state + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_domain.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_domain.py new file mode 100644 index 000000000..9de249061 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_domain.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="PublishDomain") + + +@_attrs_define +class PublishDomain: + """Publish trunk entity. + + Attributes: + ref_id (str): A generic entity id. + version (int): + archived (bool): + created_time (str): A timestamp in the application. + last_modified_time (str): A timestamp in the application. + workspace_ref_id (str): + archival_reason (None | str | Unset): + archived_time (None | str | Unset): + """ + + ref_id: str + version: int + archived: bool + created_time: str + last_modified_time: str + workspace_ref_id: str + archival_reason: None | str | Unset = UNSET + archived_time: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + ref_id = self.ref_id + + version = self.version + + archived = self.archived + + created_time = self.created_time + + last_modified_time = self.last_modified_time + + workspace_ref_id = self.workspace_ref_id + + archival_reason: None | str | Unset + if isinstance(self.archival_reason, Unset): + archival_reason = UNSET + else: + archival_reason = self.archival_reason + + archived_time: None | str | Unset + if isinstance(self.archived_time, Unset): + archived_time = UNSET + else: + archived_time = self.archived_time + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "ref_id": ref_id, + "version": version, + "archived": archived, + "created_time": created_time, + "last_modified_time": last_modified_time, + "workspace_ref_id": workspace_ref_id, + } + ) + if archival_reason is not UNSET: + field_dict["archival_reason"] = archival_reason + if archived_time is not UNSET: + field_dict["archived_time"] = archived_time + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + ref_id = d.pop("ref_id") + + version = d.pop("version") + + archived = d.pop("archived") + + created_time = d.pop("created_time") + + last_modified_time = d.pop("last_modified_time") + + workspace_ref_id = d.pop("workspace_ref_id") + + def _parse_archival_reason(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + archival_reason = _parse_archival_reason(d.pop("archival_reason", UNSET)) + + def _parse_archived_time(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + archived_time = _parse_archived_time(d.pop("archived_time", UNSET)) + + publish_domain = cls( + ref_id=ref_id, + version=version, + archived=archived, + created_time=created_time, + last_modified_time=last_modified_time, + workspace_ref_id=workspace_ref_id, + archival_reason=archival_reason, + archived_time=archived_time, + ) + + publish_domain.additional_properties = d + return publish_domain + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity.py new file mode 100644 index 000000000..5441a8df7 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.publish_entity_status import PublishEntityStatus +from ..types import UNSET, Unset + +T = TypeVar("T", bound="PublishEntity") + + +@_attrs_define +class PublishEntity: + """A publish entity. + + Attributes: + ref_id (str): A generic entity id. + version (int): + archived (bool): + created_time (str): A timestamp in the application. + last_modified_time (str): A timestamp in the application. + name (str): The name of a publish entity. + publish_domain_ref_id (str): + entity_type (str): + entity_ref_id (str): A generic entity id. + external_id (str): A GUID external id for a publish entity. + status (PublishEntityStatus): The status of a publish entity. + archival_reason (None | str | Unset): + archived_time (None | str | Unset): + """ + + ref_id: str + version: int + archived: bool + created_time: str + last_modified_time: str + name: str + publish_domain_ref_id: str + entity_type: str + entity_ref_id: str + external_id: str + status: PublishEntityStatus + archival_reason: None | str | Unset = UNSET + archived_time: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + ref_id = self.ref_id + + version = self.version + + archived = self.archived + + created_time = self.created_time + + last_modified_time = self.last_modified_time + + name = self.name + + publish_domain_ref_id = self.publish_domain_ref_id + + entity_type = self.entity_type + + entity_ref_id = self.entity_ref_id + + external_id = self.external_id + + status = self.status.value + + archival_reason: None | str | Unset + if isinstance(self.archival_reason, Unset): + archival_reason = UNSET + else: + archival_reason = self.archival_reason + + archived_time: None | str | Unset + if isinstance(self.archived_time, Unset): + archived_time = UNSET + else: + archived_time = self.archived_time + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "ref_id": ref_id, + "version": version, + "archived": archived, + "created_time": created_time, + "last_modified_time": last_modified_time, + "name": name, + "publish_domain_ref_id": publish_domain_ref_id, + "entity_type": entity_type, + "entity_ref_id": entity_ref_id, + "external_id": external_id, + "status": status, + } + ) + if archival_reason is not UNSET: + field_dict["archival_reason"] = archival_reason + if archived_time is not UNSET: + field_dict["archived_time"] = archived_time + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + ref_id = d.pop("ref_id") + + version = d.pop("version") + + archived = d.pop("archived") + + created_time = d.pop("created_time") + + last_modified_time = d.pop("last_modified_time") + + name = d.pop("name") + + publish_domain_ref_id = d.pop("publish_domain_ref_id") + + entity_type = d.pop("entity_type") + + entity_ref_id = d.pop("entity_ref_id") + + external_id = d.pop("external_id") + + status = PublishEntityStatus(d.pop("status")) + + def _parse_archival_reason(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + archival_reason = _parse_archival_reason(d.pop("archival_reason", UNSET)) + + def _parse_archived_time(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + archived_time = _parse_archived_time(d.pop("archived_time", UNSET)) + + publish_entity = cls( + ref_id=ref_id, + version=version, + archived=archived, + created_time=created_time, + last_modified_time=last_modified_time, + name=name, + publish_domain_ref_id=publish_domain_ref_id, + entity_type=entity_type, + entity_ref_id=entity_ref_id, + external_id=external_id, + status=status, + archival_reason=archival_reason, + archived_time=archived_time, + ) + + publish_entity.additional_properties = d + return publish_entity + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_activate_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_activate_args.py new file mode 100644 index 000000000..b4baa3e62 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_activate_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PublishEntityActivateArgs") + + +@_attrs_define +class PublishEntityActivateArgs: + """PublishEntityActivate args. + + Attributes: + ref_id (str): A generic entity id. + """ + + ref_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + ref_id = self.ref_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "ref_id": ref_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + ref_id = d.pop("ref_id") + + publish_entity_activate_args = cls( + ref_id=ref_id, + ) + + publish_entity_activate_args.additional_properties = d + return publish_entity_activate_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_args.py new file mode 100644 index 000000000..f0bec80e4 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_args.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PublishEntityCreateArgs") + + +@_attrs_define +class PublishEntityCreateArgs: + """PublishEntityCreate args. + + Attributes: + name (str): The name of a publish entity. + entity_type (str): + entity_ref_id (str): A generic entity id. + """ + + name: str + entity_type: str + entity_ref_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + name = self.name + + entity_type = self.entity_type + + entity_ref_id = self.entity_ref_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "name": name, + "entity_type": entity_type, + "entity_ref_id": entity_ref_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + name = d.pop("name") + + entity_type = d.pop("entity_type") + + entity_ref_id = d.pop("entity_ref_id") + + publish_entity_create_args = cls( + name=name, + entity_type=entity_type, + entity_ref_id=entity_ref_id, + ) + + publish_entity_create_args.additional_properties = d + return publish_entity_create_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_result.py new file mode 100644 index 000000000..cc0d2aec8 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_result.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.publish_entity import PublishEntity + + +T = TypeVar("T", bound="PublishEntityCreateResult") + + +@_attrs_define +class PublishEntityCreateResult: + """PublishEntityCreate result. + + Attributes: + new_publish_entity (PublishEntity): A publish entity. + """ + + new_publish_entity: PublishEntity + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + new_publish_entity = self.new_publish_entity.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "new_publish_entity": new_publish_entity, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.publish_entity import PublishEntity + + d = dict(src_dict) + new_publish_entity = PublishEntity.from_dict(d.pop("new_publish_entity")) + + publish_entity_create_result = cls( + new_publish_entity=new_publish_entity, + ) + + publish_entity_create_result.additional_properties = d + return publish_entity_create_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_by_external_id_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_by_external_id_args.py new file mode 100644 index 000000000..2278cd086 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_by_external_id_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PublishEntityLoadByExternalIdArgs") + + +@_attrs_define +class PublishEntityLoadByExternalIdArgs: + """PublishEntityLoadByExternalId args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + publish_entity_load_by_external_id_args = cls( + external_id=external_id, + ) + + publish_entity_load_by_external_id_args.additional_properties = d + return publish_entity_load_by_external_id_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_by_external_id_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_by_external_id_result.py new file mode 100644 index 000000000..fabcd1b55 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_by_external_id_result.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.publish_entity import PublishEntity + + +T = TypeVar("T", bound="PublishEntityLoadByExternalIdResult") + + +@_attrs_define +class PublishEntityLoadByExternalIdResult: + """PublishEntityLoadByExternalId result. + + Attributes: + publish_entity (PublishEntity): A publish entity. + """ + + publish_entity: PublishEntity + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + publish_entity = self.publish_entity.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "publish_entity": publish_entity, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.publish_entity import PublishEntity + + d = dict(src_dict) + publish_entity = PublishEntity.from_dict(d.pop("publish_entity")) + + publish_entity_load_by_external_id_result = cls( + publish_entity=publish_entity, + ) + + publish_entity_load_by_external_id_result.additional_properties = d + return publish_entity_load_by_external_id_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_status.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_status.py new file mode 100644 index 000000000..0bce027dd --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_status.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class PublishEntityStatus(str, Enum): + ACTIVE = "active" + DRAFT = "draft" + + def __str__(self) -> str: + return str(self.value) diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_to_draft_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_to_draft_args.py new file mode 100644 index 000000000..2e62b8b72 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_to_draft_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PublishEntityToDraftArgs") + + +@_attrs_define +class PublishEntityToDraftArgs: + """PublishEntityToDraft args. + + Attributes: + ref_id (str): A generic entity id. + """ + + ref_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + ref_id = self.ref_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "ref_id": ref_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + ref_id = d.pop("ref_id") + + publish_entity_to_draft_args = cls( + ref_id=ref_id, + ) + + publish_entity_to_draft_args.additional_properties = d + return publish_entity_to_draft_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/ts/webapi-client/gen/ApiClient.ts b/gen/ts/webapi-client/gen/ApiClient.ts index 32e26f037..d829b0f8b 100644 --- a/gen/ts/webapi-client/gen/ApiClient.ts +++ b/gen/ts/webapi-client/gen/ApiClient.ts @@ -26,6 +26,7 @@ import { MetricsService } from './services/MetricsService'; import { MotdService } from './services/MotdService'; import { NotesService } from './services/NotesService'; import { PrmService } from './services/PrmService'; +import { PublishService } from './services/PublishService'; import { PushIntegrationsService } from './services/PushIntegrationsService'; import { ReportService } from './services/ReportService'; import { ScheduleService } from './services/ScheduleService'; @@ -64,6 +65,7 @@ export class ApiClient { public readonly motd: MotdService; public readonly notes: NotesService; public readonly prm: PrmService; + public readonly publish: PublishService; public readonly pushIntegrations: PushIntegrationsService; public readonly report: ReportService; public readonly schedule: ScheduleService; @@ -113,6 +115,7 @@ export class ApiClient { this.motd = new MotdService(this.request); this.notes = new NotesService(this.request); this.prm = new PrmService(this.request); + this.publish = new PublishService(this.request); this.pushIntegrations = new PushIntegrationsService(this.request); this.report = new ReportService(this.request); this.schedule = new ScheduleService(this.request); diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index b5b9f8bcc..6539aefb4 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -25,7 +25,7 @@ export type { APIKeyLoadResult } from './models/APIKeyLoadResult'; export type { APIKeyName } from './models/APIKeyName'; export type { APIKeySummary } from './models/APIKeySummary'; export type { APIKeyUpdateArgs } from './models/APIKeyUpdateArgs'; -export { AppComponent } from './models/AppComponent'; +export type { AppComponent } from './models/AppComponent'; export { AppCore } from './models/AppCore'; export { AppDistribution } from './models/AppDistribution'; export { AppDistributionState } from './models/AppDistributionState'; @@ -254,6 +254,7 @@ export type { GoalSummary } from './models/GoalSummary'; export type { GoalUpdateArgs } from './models/GoalUpdateArgs'; export type { GoogleAuthCode } from './models/GoogleAuthCode'; export type { GoogleIdTokenClaims } from './models/GoogleIdTokenClaims'; +export type { GoogleOauthRedirectState } from './models/GoogleOauthRedirectState'; export type { GoogleOAuthTokenResponse } from './models/GoogleOAuthTokenResponse'; export type { GoogleRefreshTokenPlain } from './models/GoogleRefreshTokenPlain'; export type { GoogleSubjectId } from './models/GoogleSubjectId'; @@ -493,6 +494,17 @@ export type { PersonSummary } from './models/PersonSummary'; export type { PersonUpdateArgs } from './models/PersonUpdateArgs'; export type { PlannedTimeAndEffortSummary } from './models/PlannedTimeAndEffortSummary'; export type { PRM } from './models/PRM'; +export type { PublishDomain } from './models/PublishDomain'; +export type { PublishEntity } from './models/PublishEntity'; +export type { PublishEntityActivateArgs } from './models/PublishEntityActivateArgs'; +export type { PublishEntityCreateArgs } from './models/PublishEntityCreateArgs'; +export type { PublishEntityCreateResult } from './models/PublishEntityCreateResult'; +export type { PublishEntityLoadByExternalIdArgs } from './models/PublishEntityLoadByExternalIdArgs'; +export type { PublishEntityLoadByExternalIdResult } from './models/PublishEntityLoadByExternalIdResult'; +export type { PublishEntityName } from './models/PublishEntityName'; +export { PublishEntityStatus } from './models/PublishEntityStatus'; +export type { PublishEntityToDraftArgs } from './models/PublishEntityToDraftArgs'; +export type { PublishExternalId } from './models/PublishExternalId'; export type { PushGenerationExtraInfo } from './models/PushGenerationExtraInfo'; export type { PushIntegrationGroup } from './models/PushIntegrationGroup'; export { QuoteBlock } from './models/QuoteBlock'; @@ -850,6 +862,7 @@ export { MetricsService } from './services/MetricsService'; export { MotdService } from './services/MotdService'; export { NotesService } from './services/NotesService'; export { PrmService } from './services/PrmService'; +export { PublishService } from './services/PublishService'; export { PushIntegrationsService } from './services/PushIntegrationsService'; export { ReportService } from './services/ReportService'; export { ScheduleService } from './services/ScheduleService'; diff --git a/gen/ts/webapi-client/gen/models/AppComponent.ts b/gen/ts/webapi-client/gen/models/AppComponent.ts index ba3df57d7..67ed5cbf7 100644 --- a/gen/ts/webapi-client/gen/models/AppComponent.ts +++ b/gen/ts/webapi-client/gen/models/AppComponent.ts @@ -5,18 +5,4 @@ /** * The component of the app. */ -export enum AppComponent { - WEB = 'web', - CLI = 'cli', - APP = 'app', - GC_CRON = 'gc-cron', - GEN_CRON = 'gen-cron', - STATS_CRON = 'stats-cron', - SCHEDULE_EXTERNAL_SYNC_CRON = 'schedule-external-sync-cron', - SEARCH_INDEX_BACKFILL = 'search-index-backfill', - CRM_BACKFILL = 'crm-backfill', - SEARCH_MUTATION_LOG_DRAIN = 'search-mutation-log-drain', - SEARCH_MUTATION_REQUEUE = 'search-mutation-requeue', - CLEAR_ABANDONED_USERS_CRON = 'clear-abandoned-users-cron', - SYNC_GOOGLE_USER_DATA_DO_ALL = 'sync-google-user-data-do-all', -} +export type AppComponent = string; diff --git a/gen/ts/webapi-client/gen/models/GoogleOauthRedirectState.ts b/gen/ts/webapi-client/gen/models/GoogleOauthRedirectState.ts new file mode 100644 index 000000000..878c0ce2f --- /dev/null +++ b/gen/ts/webapi-client/gen/models/GoogleOauthRedirectState.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { SystemUrl } from './SystemUrl'; +/** + * OAuth state embedding post-auth redirect targets. + */ +export type GoogleOauthRedirectState = { + nonce: string; + callback_success_url: SystemUrl; + callback_failure_url: SystemUrl; +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishDomain.ts b/gen/ts/webapi-client/gen/models/PublishDomain.ts new file mode 100644 index 000000000..016764668 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishDomain.ts @@ -0,0 +1,20 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { Timestamp } from './Timestamp'; +/** + * Publish trunk entity. + */ +export type PublishDomain = { + ref_id: EntityId; + version: number; + archived: boolean; + archival_reason?: (string | null); + created_time: Timestamp; + last_modified_time: Timestamp; + archived_time?: (Timestamp | null); + workspace_ref_id: string; +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntity.ts b/gen/ts/webapi-client/gen/models/PublishEntity.ts new file mode 100644 index 000000000..27b133c31 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntity.ts @@ -0,0 +1,28 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { PublishEntityName } from './PublishEntityName'; +import type { PublishEntityStatus } from './PublishEntityStatus'; +import type { PublishExternalId } from './PublishExternalId'; +import type { Timestamp } from './Timestamp'; +/** + * A publish entity. + */ +export type PublishEntity = { + ref_id: EntityId; + version: number; + archived: boolean; + archival_reason?: (string | null); + created_time: Timestamp; + last_modified_time: Timestamp; + archived_time?: (Timestamp | null); + name: PublishEntityName; + publish_domain_ref_id: string; + entity_type: string; + entity_ref_id: EntityId; + external_id: PublishExternalId; + status: PublishEntityStatus; +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntityActivateArgs.ts b/gen/ts/webapi-client/gen/models/PublishEntityActivateArgs.ts new file mode 100644 index 000000000..649bb1a80 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityActivateArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +/** + * PublishEntityActivate args. + */ +export type PublishEntityActivateArgs = { + ref_id: EntityId; +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntityCreateArgs.ts b/gen/ts/webapi-client/gen/models/PublishEntityCreateArgs.ts new file mode 100644 index 000000000..bf7acd8af --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityCreateArgs.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { PublishEntityName } from './PublishEntityName'; +/** + * PublishEntityCreate args. + */ +export type PublishEntityCreateArgs = { + name: PublishEntityName; + entity_type: string; + entity_ref_id: EntityId; +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntityCreateResult.ts b/gen/ts/webapi-client/gen/models/PublishEntityCreateResult.ts new file mode 100644 index 000000000..657fd0eb8 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityCreateResult.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishEntity } from './PublishEntity'; +/** + * PublishEntityCreate result. + */ +export type PublishEntityCreateResult = { + new_publish_entity: PublishEntity; +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntityLoadByExternalIdArgs.ts b/gen/ts/webapi-client/gen/models/PublishEntityLoadByExternalIdArgs.ts new file mode 100644 index 000000000..7a762dc9d --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityLoadByExternalIdArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * PublishEntityLoadByExternalId args. + */ +export type PublishEntityLoadByExternalIdArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntityLoadByExternalIdResult.ts b/gen/ts/webapi-client/gen/models/PublishEntityLoadByExternalIdResult.ts new file mode 100644 index 000000000..d7d50036c --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityLoadByExternalIdResult.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishEntity } from './PublishEntity'; +/** + * PublishEntityLoadByExternalId result. + */ +export type PublishEntityLoadByExternalIdResult = { + publish_entity: PublishEntity; +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntityName.ts b/gen/ts/webapi-client/gen/models/PublishEntityName.ts new file mode 100644 index 000000000..03df67ab6 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityName.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * The name of a publish entity. + */ +export type PublishEntityName = string; diff --git a/gen/ts/webapi-client/gen/models/PublishEntityStatus.ts b/gen/ts/webapi-client/gen/models/PublishEntityStatus.ts new file mode 100644 index 000000000..e3ccdba65 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityStatus.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * The status of a publish entity. + */ +export enum PublishEntityStatus { + DRAFT = 'draft', + ACTIVE = 'active', +} diff --git a/gen/ts/webapi-client/gen/models/PublishEntityToDraftArgs.ts b/gen/ts/webapi-client/gen/models/PublishEntityToDraftArgs.ts new file mode 100644 index 000000000..fed4ae9f1 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityToDraftArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +/** + * PublishEntityToDraft args. + */ +export type PublishEntityToDraftArgs = { + ref_id: EntityId; +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishExternalId.ts b/gen/ts/webapi-client/gen/models/PublishExternalId.ts new file mode 100644 index 000000000..268072151 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishExternalId.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * A GUID external id for a publish entity. + */ +export type PublishExternalId = string; diff --git a/gen/ts/webapi-client/gen/services/ApiKeyService.ts b/gen/ts/webapi-client/gen/services/ApiKeyService.ts index 7ee8a8f82..359d1dde2 100644 --- a/gen/ts/webapi-client/gen/services/ApiKeyService.ts +++ b/gen/ts/webapi-client/gen/services/ApiKeyService.ts @@ -35,7 +35,7 @@ export class ApiKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -63,7 +63,7 @@ export class ApiKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -91,7 +91,7 @@ export class ApiKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -119,7 +119,7 @@ export class ApiKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -147,7 +147,7 @@ export class ApiKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -175,7 +175,7 @@ export class ApiKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/ApplicationService.ts b/gen/ts/webapi-client/gen/services/ApplicationService.ts index 32ae047e8..0ab622c1d 100644 --- a/gen/ts/webapi-client/gen/services/ApplicationService.ts +++ b/gen/ts/webapi-client/gen/services/ApplicationService.ts @@ -43,7 +43,7 @@ export class ApplicationService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -71,7 +71,7 @@ export class ApplicationService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -99,7 +99,7 @@ export class ApplicationService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -127,7 +127,7 @@ export class ApplicationService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -155,7 +155,7 @@ export class ApplicationService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -183,7 +183,7 @@ export class ApplicationService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -211,7 +211,7 @@ export class ApplicationService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -239,7 +239,7 @@ export class ApplicationService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -267,7 +267,7 @@ export class ApplicationService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -295,7 +295,7 @@ export class ApplicationService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/AuthService.ts b/gen/ts/webapi-client/gen/services/AuthService.ts index fd0e7009a..bf118fa5e 100644 --- a/gen/ts/webapi-client/gen/services/AuthService.ts +++ b/gen/ts/webapi-client/gen/services/AuthService.ts @@ -33,7 +33,7 @@ export class AuthService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -61,7 +61,7 @@ export class AuthService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -89,7 +89,7 @@ export class AuthService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -117,7 +117,7 @@ export class AuthService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -145,7 +145,7 @@ export class AuthService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/BigPlansService.ts b/gen/ts/webapi-client/gen/services/BigPlansService.ts index 08d7f690f..e86a5f34d 100644 --- a/gen/ts/webapi-client/gen/services/BigPlansService.ts +++ b/gen/ts/webapi-client/gen/services/BigPlansService.ts @@ -45,7 +45,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -73,7 +73,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -101,7 +101,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -129,7 +129,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -157,7 +157,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -185,7 +185,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -213,7 +213,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -241,7 +241,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -269,7 +269,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -297,7 +297,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -325,7 +325,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -353,7 +353,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -381,7 +381,7 @@ export class BigPlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/CalendarService.ts b/gen/ts/webapi-client/gen/services/CalendarService.ts index 83aee417f..ec47d73f4 100644 --- a/gen/ts/webapi-client/gen/services/CalendarService.ts +++ b/gen/ts/webapi-client/gen/services/CalendarService.ts @@ -27,7 +27,7 @@ export class CalendarService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/ChoresService.ts b/gen/ts/webapi-client/gen/services/ChoresService.ts index df5f4e828..af0c3495c 100644 --- a/gen/ts/webapi-client/gen/services/ChoresService.ts +++ b/gen/ts/webapi-client/gen/services/ChoresService.ts @@ -37,7 +37,7 @@ export class ChoresService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -65,7 +65,7 @@ export class ChoresService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -93,7 +93,7 @@ export class ChoresService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -121,7 +121,7 @@ export class ChoresService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -149,7 +149,7 @@ export class ChoresService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -177,7 +177,7 @@ export class ChoresService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -205,7 +205,7 @@ export class ChoresService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -233,7 +233,7 @@ export class ChoresService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -261,7 +261,7 @@ export class ChoresService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/ContactsService.ts b/gen/ts/webapi-client/gen/services/ContactsService.ts index 93f184039..02ad0efdd 100644 --- a/gen/ts/webapi-client/gen/services/ContactsService.ts +++ b/gen/ts/webapi-client/gen/services/ContactsService.ts @@ -36,7 +36,7 @@ export class ContactsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -64,7 +64,7 @@ export class ContactsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -92,7 +92,7 @@ export class ContactsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -120,7 +120,7 @@ export class ContactsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -148,7 +148,7 @@ export class ContactsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -176,7 +176,7 @@ export class ContactsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -204,7 +204,7 @@ export class ContactsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/DocsService.ts b/gen/ts/webapi-client/gen/services/DocsService.ts index dc0152235..8487462c6 100644 --- a/gen/ts/webapi-client/gen/services/DocsService.ts +++ b/gen/ts/webapi-client/gen/services/DocsService.ts @@ -43,7 +43,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -71,7 +71,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -99,7 +99,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -127,7 +127,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -155,7 +155,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -183,7 +183,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -211,7 +211,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -239,7 +239,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -267,7 +267,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -295,7 +295,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -323,7 +323,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -351,7 +351,7 @@ export class DocsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/GcService.ts b/gen/ts/webapi-client/gen/services/GcService.ts index 807d30b72..bdd972240 100644 --- a/gen/ts/webapi-client/gen/services/GcService.ts +++ b/gen/ts/webapi-client/gen/services/GcService.ts @@ -28,7 +28,7 @@ export class GcService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -56,7 +56,7 @@ export class GcService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/GenService.ts b/gen/ts/webapi-client/gen/services/GenService.ts index 103d200ab..1c346beeb 100644 --- a/gen/ts/webapi-client/gen/services/GenService.ts +++ b/gen/ts/webapi-client/gen/services/GenService.ts @@ -28,7 +28,7 @@ export class GenService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -56,7 +56,7 @@ export class GenService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/HabitsService.ts b/gen/ts/webapi-client/gen/services/HabitsService.ts index 1260d46ad..311147f3d 100644 --- a/gen/ts/webapi-client/gen/services/HabitsService.ts +++ b/gen/ts/webapi-client/gen/services/HabitsService.ts @@ -37,7 +37,7 @@ export class HabitsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -65,7 +65,7 @@ export class HabitsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -93,7 +93,7 @@ export class HabitsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -121,7 +121,7 @@ export class HabitsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -149,7 +149,7 @@ export class HabitsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -177,7 +177,7 @@ export class HabitsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -205,7 +205,7 @@ export class HabitsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -233,7 +233,7 @@ export class HabitsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -261,7 +261,7 @@ export class HabitsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/HomeService.ts b/gen/ts/webapi-client/gen/services/HomeService.ts index 890032878..06c9d8a99 100644 --- a/gen/ts/webapi-client/gen/services/HomeService.ts +++ b/gen/ts/webapi-client/gen/services/HomeService.ts @@ -42,7 +42,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -70,7 +70,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -98,7 +98,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -126,7 +126,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -154,7 +154,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -182,7 +182,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -210,7 +210,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -238,7 +238,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -266,7 +266,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -294,7 +294,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -322,7 +322,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -350,7 +350,7 @@ export class HomeService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/InboxTasksService.ts b/gen/ts/webapi-client/gen/services/InboxTasksService.ts index 1bd06d42b..49a646491 100644 --- a/gen/ts/webapi-client/gen/services/InboxTasksService.ts +++ b/gen/ts/webapi-client/gen/services/InboxTasksService.ts @@ -33,7 +33,7 @@ export class InboxTasksService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -61,7 +61,7 @@ export class InboxTasksService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -89,7 +89,7 @@ export class InboxTasksService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -117,7 +117,7 @@ export class InboxTasksService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -145,7 +145,7 @@ export class InboxTasksService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/InfraService.ts b/gen/ts/webapi-client/gen/services/InfraService.ts index c07ebbd64..c454d3e43 100644 --- a/gen/ts/webapi-client/gen/services/InfraService.ts +++ b/gen/ts/webapi-client/gen/services/InfraService.ts @@ -31,7 +31,7 @@ export class InfraService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -59,7 +59,7 @@ export class InfraService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -87,7 +87,7 @@ export class InfraService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/JournalsService.ts b/gen/ts/webapi-client/gen/services/JournalsService.ts index 1a847133b..9b069c227 100644 --- a/gen/ts/webapi-client/gen/services/JournalsService.ts +++ b/gen/ts/webapi-client/gen/services/JournalsService.ts @@ -41,7 +41,7 @@ export class JournalsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -69,7 +69,7 @@ export class JournalsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -97,7 +97,7 @@ export class JournalsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -125,7 +125,7 @@ export class JournalsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -153,7 +153,7 @@ export class JournalsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -181,7 +181,7 @@ export class JournalsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -209,7 +209,7 @@ export class JournalsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -237,7 +237,7 @@ export class JournalsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -265,7 +265,7 @@ export class JournalsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -293,7 +293,7 @@ export class JournalsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -321,7 +321,7 @@ export class JournalsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/LifePlanService.ts b/gen/ts/webapi-client/gen/services/LifePlanService.ts index d35f581ef..a1b099414 100644 --- a/gen/ts/webapi-client/gen/services/LifePlanService.ts +++ b/gen/ts/webapi-client/gen/services/LifePlanService.ts @@ -78,7 +78,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -106,7 +106,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -134,7 +134,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -162,7 +162,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -190,7 +190,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -218,7 +218,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -246,7 +246,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -274,7 +274,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -302,7 +302,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -330,7 +330,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -358,7 +358,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -386,7 +386,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -414,7 +414,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -442,7 +442,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -470,7 +470,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -498,7 +498,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -526,7 +526,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -554,7 +554,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -582,7 +582,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -610,7 +610,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -638,7 +638,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -666,7 +666,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -694,7 +694,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -722,7 +722,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -750,7 +750,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -778,7 +778,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -806,7 +806,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -834,7 +834,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -862,7 +862,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -890,7 +890,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -918,7 +918,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -946,7 +946,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -974,7 +974,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -1002,7 +1002,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -1030,7 +1030,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -1058,7 +1058,7 @@ export class LifePlanService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/McpKeyService.ts b/gen/ts/webapi-client/gen/services/McpKeyService.ts index 13bbe2f2d..7604ea56c 100644 --- a/gen/ts/webapi-client/gen/services/McpKeyService.ts +++ b/gen/ts/webapi-client/gen/services/McpKeyService.ts @@ -35,7 +35,7 @@ export class McpKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -63,7 +63,7 @@ export class McpKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -91,7 +91,7 @@ export class McpKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -119,7 +119,7 @@ export class McpKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -147,7 +147,7 @@ export class McpKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -175,7 +175,7 @@ export class McpKeyService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/MetricsService.ts b/gen/ts/webapi-client/gen/services/MetricsService.ts index 9c751c9d7..939b490fe 100644 --- a/gen/ts/webapi-client/gen/services/MetricsService.ts +++ b/gen/ts/webapi-client/gen/services/MetricsService.ts @@ -44,7 +44,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -72,7 +72,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -100,7 +100,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -128,7 +128,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -156,7 +156,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -184,7 +184,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -212,7 +212,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -240,7 +240,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -268,7 +268,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -296,7 +296,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -324,7 +324,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -352,7 +352,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -380,7 +380,7 @@ export class MetricsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/MotdService.ts b/gen/ts/webapi-client/gen/services/MotdService.ts index 465b84f48..0093ff819 100644 --- a/gen/ts/webapi-client/gen/services/MotdService.ts +++ b/gen/ts/webapi-client/gen/services/MotdService.ts @@ -27,7 +27,7 @@ export class MotdService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/NotesService.ts b/gen/ts/webapi-client/gen/services/NotesService.ts index e95c0a7cb..0a96d433e 100644 --- a/gen/ts/webapi-client/gen/services/NotesService.ts +++ b/gen/ts/webapi-client/gen/services/NotesService.ts @@ -36,7 +36,7 @@ export class NotesService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -64,7 +64,7 @@ export class NotesService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -92,7 +92,7 @@ export class NotesService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -120,7 +120,7 @@ export class NotesService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -148,7 +148,7 @@ export class NotesService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -176,7 +176,7 @@ export class NotesService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -204,7 +204,7 @@ export class NotesService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/PrmService.ts b/gen/ts/webapi-client/gen/services/PrmService.ts index 641f9df14..72261cc39 100644 --- a/gen/ts/webapi-client/gen/services/PrmService.ts +++ b/gen/ts/webapi-client/gen/services/PrmService.ts @@ -53,7 +53,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -81,7 +81,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -109,7 +109,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -137,7 +137,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -165,7 +165,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -193,7 +193,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -221,7 +221,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -249,7 +249,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -277,7 +277,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -305,7 +305,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -333,7 +333,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -361,7 +361,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -389,7 +389,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -417,7 +417,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -445,7 +445,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -473,7 +473,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -501,7 +501,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -529,7 +529,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -557,7 +557,7 @@ export class PrmService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/PublishService.ts b/gen/ts/webapi-client/gen/services/PublishService.ts new file mode 100644 index 000000000..685e2e39d --- /dev/null +++ b/gen/ts/webapi-client/gen/services/PublishService.ts @@ -0,0 +1,127 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishEntityActivateArgs } from '../models/PublishEntityActivateArgs'; +import type { PublishEntityCreateArgs } from '../models/PublishEntityCreateArgs'; +import type { PublishEntityCreateResult } from '../models/PublishEntityCreateResult'; +import type { PublishEntityLoadByExternalIdArgs } from '../models/PublishEntityLoadByExternalIdArgs'; +import type { PublishEntityLoadByExternalIdResult } from '../models/PublishEntityLoadByExternalIdResult'; +import type { PublishEntityToDraftArgs } from '../models/PublishEntityToDraftArgs'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import type { BaseHttpRequest } from '../core/BaseHttpRequest'; +export class PublishService { + constructor(public readonly httpRequest: BaseHttpRequest) {} + /** + * Use case for activating a publish entity. + * @param requestBody The input data + * @returns any Successful response / Empty body + * @throws ApiError + */ + public publishEntityActivate( + requestBody?: PublishEntityActivateArgs, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/publish-entity-activate', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } + /** + * Use case for creating a publish entity. + * @param requestBody The input data + * @returns PublishEntityCreateResult Successful response + * @throws ApiError + */ + public publishEntityCreate( + requestBody?: PublishEntityCreateArgs, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/publish-entity-create', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } + /** + * Load a publish entity by its external id. + * @param requestBody The input data + * @returns PublishEntityLoadByExternalIdResult Successful response + * @throws ApiError + */ + public publishEntityLoadByExternalId( + requestBody?: PublishEntityLoadByExternalIdArgs, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/publish-entity-load-by-external-id', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } + /** + * Use case for moving a publish entity back to draft. + * @param requestBody The input data + * @returns any Successful response / Empty body + * @throws ApiError + */ + public publishEntityToDraft( + requestBody?: PublishEntityToDraftArgs, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/publish-entity-to-draft', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } +} diff --git a/gen/ts/webapi-client/gen/services/PushIntegrationsService.ts b/gen/ts/webapi-client/gen/services/PushIntegrationsService.ts index 09d707c26..bd5706a73 100644 --- a/gen/ts/webapi-client/gen/services/PushIntegrationsService.ts +++ b/gen/ts/webapi-client/gen/services/PushIntegrationsService.ts @@ -43,7 +43,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -71,7 +71,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -99,7 +99,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -127,7 +127,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -155,7 +155,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -183,7 +183,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -211,7 +211,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -239,7 +239,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -267,7 +267,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -295,7 +295,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -323,7 +323,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -351,7 +351,7 @@ export class PushIntegrationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/ReportService.ts b/gen/ts/webapi-client/gen/services/ReportService.ts index 14b6dcb3b..298b0aff8 100644 --- a/gen/ts/webapi-client/gen/services/ReportService.ts +++ b/gen/ts/webapi-client/gen/services/ReportService.ts @@ -27,7 +27,7 @@ export class ReportService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/ScheduleService.ts b/gen/ts/webapi-client/gen/services/ScheduleService.ts index 440a11df3..b3b05ca65 100644 --- a/gen/ts/webapi-client/gen/services/ScheduleService.ts +++ b/gen/ts/webapi-client/gen/services/ScheduleService.ts @@ -66,7 +66,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -94,7 +94,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -122,7 +122,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -150,7 +150,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -178,7 +178,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -206,7 +206,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -234,7 +234,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -262,7 +262,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -290,7 +290,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -318,7 +318,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -346,7 +346,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -374,7 +374,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -402,7 +402,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -430,7 +430,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -458,7 +458,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -486,7 +486,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -514,7 +514,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -542,7 +542,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -570,7 +570,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -598,7 +598,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -626,7 +626,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -654,7 +654,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -682,7 +682,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -710,7 +710,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -738,7 +738,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -766,7 +766,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -794,7 +794,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -822,7 +822,7 @@ export class ScheduleService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/SearchService.ts b/gen/ts/webapi-client/gen/services/SearchService.ts index a7e6c38aa..83ef1267e 100644 --- a/gen/ts/webapi-client/gen/services/SearchService.ts +++ b/gen/ts/webapi-client/gen/services/SearchService.ts @@ -27,7 +27,7 @@ export class SearchService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/SmartListsService.ts b/gen/ts/webapi-client/gen/services/SmartListsService.ts index 89ec25fcb..33defa932 100644 --- a/gen/ts/webapi-client/gen/services/SmartListsService.ts +++ b/gen/ts/webapi-client/gen/services/SmartListsService.ts @@ -41,7 +41,7 @@ export class SmartListsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -69,7 +69,7 @@ export class SmartListsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -97,7 +97,7 @@ export class SmartListsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -125,7 +125,7 @@ export class SmartListsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -153,7 +153,7 @@ export class SmartListsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -181,7 +181,7 @@ export class SmartListsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -209,7 +209,7 @@ export class SmartListsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -237,7 +237,7 @@ export class SmartListsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -265,7 +265,7 @@ export class SmartListsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -293,7 +293,7 @@ export class SmartListsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -321,7 +321,7 @@ export class SmartListsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/StatsService.ts b/gen/ts/webapi-client/gen/services/StatsService.ts index f6390f8b5..4430f56c9 100644 --- a/gen/ts/webapi-client/gen/services/StatsService.ts +++ b/gen/ts/webapi-client/gen/services/StatsService.ts @@ -28,7 +28,7 @@ export class StatsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -56,7 +56,7 @@ export class StatsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/TagsService.ts b/gen/ts/webapi-client/gen/services/TagsService.ts index 6b7e5eb89..4ccdaf1fa 100644 --- a/gen/ts/webapi-client/gen/services/TagsService.ts +++ b/gen/ts/webapi-client/gen/services/TagsService.ts @@ -36,7 +36,7 @@ export class TagsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -64,7 +64,7 @@ export class TagsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -92,7 +92,7 @@ export class TagsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -120,7 +120,7 @@ export class TagsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -148,7 +148,7 @@ export class TagsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -176,7 +176,7 @@ export class TagsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -204,7 +204,7 @@ export class TagsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/TestHelperService.ts b/gen/ts/webapi-client/gen/services/TestHelperService.ts index a01a767ad..7f0c58172 100644 --- a/gen/ts/webapi-client/gen/services/TestHelperService.ts +++ b/gen/ts/webapi-client/gen/services/TestHelperService.ts @@ -30,7 +30,7 @@ export class TestHelperService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -58,7 +58,7 @@ export class TestHelperService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -86,7 +86,7 @@ export class TestHelperService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -118,7 +118,7 @@ export class TestHelperService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -146,7 +146,7 @@ export class TestHelperService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/TimeEventsService.ts b/gen/ts/webapi-client/gen/services/TimeEventsService.ts index 75405496f..b679c230b 100644 --- a/gen/ts/webapi-client/gen/services/TimeEventsService.ts +++ b/gen/ts/webapi-client/gen/services/TimeEventsService.ts @@ -42,7 +42,7 @@ export class TimeEventsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -70,7 +70,7 @@ export class TimeEventsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -98,7 +98,7 @@ export class TimeEventsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -126,7 +126,7 @@ export class TimeEventsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -154,7 +154,7 @@ export class TimeEventsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -182,7 +182,7 @@ export class TimeEventsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -210,7 +210,7 @@ export class TimeEventsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -238,7 +238,7 @@ export class TimeEventsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -266,7 +266,7 @@ export class TimeEventsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -294,7 +294,7 @@ export class TimeEventsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/TimePlansService.ts b/gen/ts/webapi-client/gen/services/TimePlansService.ts index 3f3e4edbd..c17c5353b 100644 --- a/gen/ts/webapi-client/gen/services/TimePlansService.ts +++ b/gen/ts/webapi-client/gen/services/TimePlansService.ts @@ -58,7 +58,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -86,7 +86,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -114,7 +114,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -142,7 +142,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -170,7 +170,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -198,7 +198,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -226,7 +226,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -254,7 +254,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -282,7 +282,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -310,7 +310,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -338,7 +338,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -366,7 +366,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -394,7 +394,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -422,7 +422,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -450,7 +450,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -478,7 +478,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -506,7 +506,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -534,7 +534,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -562,7 +562,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -590,7 +590,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -618,7 +618,7 @@ export class TimePlansService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/TodoService.ts b/gen/ts/webapi-client/gen/services/TodoService.ts index 4d76ab632..de18a2165 100644 --- a/gen/ts/webapi-client/gen/services/TodoService.ts +++ b/gen/ts/webapi-client/gen/services/TodoService.ts @@ -35,7 +35,7 @@ export class TodoService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -63,7 +63,7 @@ export class TodoService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -91,7 +91,7 @@ export class TodoService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -119,7 +119,7 @@ export class TodoService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -147,7 +147,7 @@ export class TodoService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -175,7 +175,7 @@ export class TodoService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/UsersService.ts b/gen/ts/webapi-client/gen/services/UsersService.ts index 61918751d..cc1247c1d 100644 --- a/gen/ts/webapi-client/gen/services/UsersService.ts +++ b/gen/ts/webapi-client/gen/services/UsersService.ts @@ -32,7 +32,7 @@ export class UsersService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -60,7 +60,7 @@ export class UsersService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -88,7 +88,7 @@ export class UsersService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -116,7 +116,7 @@ export class UsersService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -144,7 +144,7 @@ export class UsersService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/VacationsService.ts b/gen/ts/webapi-client/gen/services/VacationsService.ts index fee487906..958d60059 100644 --- a/gen/ts/webapi-client/gen/services/VacationsService.ts +++ b/gen/ts/webapi-client/gen/services/VacationsService.ts @@ -34,7 +34,7 @@ export class VacationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -62,7 +62,7 @@ export class VacationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -90,7 +90,7 @@ export class VacationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -118,7 +118,7 @@ export class VacationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -146,7 +146,7 @@ export class VacationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -174,7 +174,7 @@ export class VacationsService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/WorkingMemService.ts b/gen/ts/webapi-client/gen/services/WorkingMemService.ts index d7f1a23f6..666a97be2 100644 --- a/gen/ts/webapi-client/gen/services/WorkingMemService.ts +++ b/gen/ts/webapi-client/gen/services/WorkingMemService.ts @@ -30,7 +30,7 @@ export class WorkingMemService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -58,7 +58,7 @@ export class WorkingMemService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -86,7 +86,7 @@ export class WorkingMemService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/gen/ts/webapi-client/gen/services/WorkspacesService.ts b/gen/ts/webapi-client/gen/services/WorkspacesService.ts index 7f561361e..af2072460 100644 --- a/gen/ts/webapi-client/gen/services/WorkspacesService.ts +++ b/gen/ts/webapi-client/gen/services/WorkspacesService.ts @@ -29,7 +29,7 @@ export class WorkspacesService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -57,7 +57,7 @@ export class WorkspacesService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, @@ -85,7 +85,7 @@ export class WorkspacesService { 401: `Error response for ExpiredAuthTokenError`, 404: `Error response for EntityNotFoundError`, 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, - 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, 426: `Error response for InvalidAuthTokenError`, diff --git a/itests/api/common/publish.test.py b/itests/api/common/publish.test.py new file mode 100644 index 000000000..5d999790a --- /dev/null +++ b/itests/api/common/publish.test.py @@ -0,0 +1,252 @@ +"""Tests for the publish API.""" + +from collections.abc import Iterator + +import pytest +import requests +from jupiter_webapi_client.api.publish.publish_entity_activate import ( + sync_detailed as publish_entity_activate_sync, +) +from jupiter_webapi_client.api.publish.publish_entity_create import ( + sync_detailed as publish_entity_create_sync, +) +from jupiter_webapi_client.api.prm.person_create import ( + sync_detailed as person_create_sync, +) +from jupiter_webapi_client.api.test_helper.workspace_set_feature import ( + sync_detailed as workspace_set_feature_sync, +) +from jupiter_webapi_client.client import AuthenticatedClient +from jupiter_webapi_client.models.person import Person +from jupiter_webapi_client.models.person_create_args import PersonCreateArgs +from jupiter_webapi_client.models.person_create_result import PersonCreateResult +from jupiter_webapi_client.models.publish_entity import PublishEntity +from jupiter_webapi_client.models.publish_entity_activate_args import ( + PublishEntityActivateArgs, +) +from jupiter_webapi_client.models.publish_entity_create_args import ( + PublishEntityCreateArgs, +) +from jupiter_webapi_client.models.publish_entity_create_result import ( + PublishEntityCreateResult, +) +from jupiter_webapi_client.models.workspace_feature import WorkspaceFeature +from jupiter_webapi_client.models.workspace_set_feature_args import ( + WorkspaceSetFeatureArgs, +) + +from itests.helpers import get_parsed_from_response + +_SHAREABLE_ENTITY_TYPE = "Person" + + +@pytest.fixture(autouse=True, scope="module") +def _enable_prm_feature(logged_in_client: AuthenticatedClient) -> Iterator[None]: + try: + workspace_set_feature_sync( + client=logged_in_client, + body=WorkspaceSetFeatureArgs(feature=WorkspaceFeature.PRM, value=True), + ) + yield + finally: + workspace_set_feature_sync( + client=logged_in_client, + body=WorkspaceSetFeatureArgs(feature=WorkspaceFeature.PRM, value=False), + ) + + +@pytest.fixture() +def create_person(logged_in_client: AuthenticatedClient): + def _create(name: str) -> Person: + result = person_create_sync( + client=logged_in_client, + body=PersonCreateArgs(name=name), + ) + return get_parsed_from_response(PersonCreateResult, result).new_person + + return _create + + +@pytest.fixture() +def create_publish_entity(logged_in_client: AuthenticatedClient, create_person): + def _create( + name: str, entity_type: str = _SHAREABLE_ENTITY_TYPE + ) -> PublishEntity: + person = create_person(f"person-for-{name}") + result = publish_entity_create_sync( + client=logged_in_client, + body=PublishEntityCreateArgs( + name=name, + entity_type=entity_type, + entity_ref_id=person.ref_id, + ), + ) + return get_parsed_from_response( + PublishEntityCreateResult, result + ).new_publish_entity + + return _create + + +@pytest.fixture() +def activate_publish_entity(logged_in_client: AuthenticatedClient): + def _activate(ref_id: str) -> None: + result = publish_entity_activate_sync( + client=logged_in_client, + body=PublishEntityActivateArgs(ref_id=ref_id), + ) + assert result.status_code == 200 + + return _activate + + +def _headers(api_key: str) -> dict[str, str]: + return {"Authorization": f"Bearer {api_key}"} + + +def _publish_entities_url(api_url: str) -> str: + return f"{api_url}/v1/common/publish/entities" + + +def test_api_common_publish_entity_create( + api_url: str, api_key: str, create_person +) -> None: + person = create_person("person-for-draft-publish") + + response = requests.post( + _publish_entities_url(api_url), + headers=_headers(api_key), + json={ + "name": "draft-publish", + "entity_type": _SHAREABLE_ENTITY_TYPE, + "entity_ref_id": person.ref_id, + }, + timeout=10, + ) + assert response.status_code == 200 + + publish_entity = response.json()["new_publish_entity"] + assert publish_entity["status"] == "draft" + assert publish_entity["external_id"] + + +def test_api_common_publish_entity_create_rejects_non_shareable_entity_type( + api_url: str, api_key: str, create_person +) -> None: + person = create_person("person-for-invalid-publish") + + response = requests.post( + _publish_entities_url(api_url), + headers=_headers(api_key), + json={ + "name": "invalid-publish", + "entity_type": "TodoTask", + "entity_ref_id": person.ref_id, + }, + timeout=10, + ) + assert response.status_code == 502 + assert response.json()["status"] == 422 + + +def test_api_common_publish_entity_load_by_external_id_active( + api_url: str, + api_key: str, + create_publish_entity, + activate_publish_entity, +) -> None: + publish_entity = create_publish_entity("active-publish") + activate_publish_entity(publish_entity.ref_id) + + response = requests.post( + f"{_publish_entities_url(api_url)}/load-by-external-id", + headers=_headers(api_key), + json={"external_id": publish_entity.external_id}, + timeout=10, + ) + assert response.status_code == 200 + + loaded = response.json()["publish_entity"] + assert loaded["ref_id"] == publish_entity.ref_id + assert loaded["status"] == "active" + + +def test_api_common_publish_entity_load_by_external_id_not_found( + api_url: str, api_key: str +) -> None: + response = requests.post( + f"{_publish_entities_url(api_url)}/load-by-external-id", + headers=_headers(api_key), + json={"external_id": "00000000-0000-4000-8000-000000000000"}, + timeout=10, + ) + assert response.status_code == 502 + assert response.json()["status"] == 404 + + +def test_api_common_publish_entity_create_duplicate_entity_raises_already_exists( + api_url: str, api_key: str, create_publish_entity +) -> None: + publish_entity = create_publish_entity("unique-publish") + + response = requests.post( + _publish_entities_url(api_url), + headers=_headers(api_key), + json={ + "name": "another-name", + "entity_type": _SHAREABLE_ENTITY_TYPE, + "entity_ref_id": publish_entity.entity_ref_id, + }, + timeout=10, + ) + assert response.status_code == 502 + assert response.json()["status"] == 400 + + +def test_api_common_publish_entity_activate_when_already_active_returns_conflict( + api_url: str, + api_key: str, + create_publish_entity, + activate_publish_entity, +) -> None: + publish_entity = create_publish_entity("activate-twice") + activate_publish_entity(publish_entity.ref_id) + + response = requests.post( + f"{_publish_entities_url(api_url)}/{publish_entity.ref_id}/activate", + headers=_headers(api_key), + json={}, + timeout=10, + ) + assert response.status_code == 502 + assert response.json()["status"] == 409 + + +def test_api_common_publish_entity_to_draft_when_already_draft_returns_conflict( + api_url: str, api_key: str, create_publish_entity +) -> None: + publish_entity = create_publish_entity("to-draft-twice") + + response = requests.post( + f"{_publish_entities_url(api_url)}/{publish_entity.ref_id}/to-draft", + headers=_headers(api_key), + json={}, + timeout=10, + ) + assert response.status_code == 502 + assert response.json()["status"] == 409 + + +def test_api_common_publish_entity_load_by_external_id_draft_not_loadable( + api_url: str, api_key: str, create_publish_entity +) -> None: + publish_entity = create_publish_entity("draft-only-publish") + + response = requests.post( + f"{_publish_entities_url(api_url)}/load-by-external-id", + headers=_headers(api_key), + json={"external_id": publish_entity.external_id}, + timeout=10, + ) + assert response.status_code == 502 + assert response.json()["status"] == 422 diff --git a/src/api/jupiter/api/jupiter.py b/src/api/jupiter/api/jupiter.py index 70d8a98e7..70e7c3907 100644 --- a/src/api/jupiter/api/jupiter.py +++ b/src/api/jupiter/api/jupiter.py @@ -558,6 +558,20 @@ asyncio_detailed as smart_list_update, ) +# --- Publish API --- +from jupiter_webapi_client.api.publish.publish_entity_activate import ( + asyncio_detailed as publish_entity_activate, +) +from jupiter_webapi_client.api.publish.publish_entity_create import ( + asyncio_detailed as publish_entity_create, +) +from jupiter_webapi_client.api.publish.publish_entity_load_by_external_id import ( + asyncio_detailed as publish_entity_load_by_external_id, +) +from jupiter_webapi_client.api.publish.publish_entity_to_draft import ( + asyncio_detailed as publish_entity_to_draft, +) + # --- Tags API --- from jupiter_webapi_client.api.tags.tag_archive import ( asyncio_detailed as tag_archive, @@ -1407,6 +1421,31 @@ async def main() -> None: ), ), ), + # Publish + JupiterApiResource.build( + "publish", + JupiterApiResource.build( + "entities", + JupiterApiGatewayMethod.post(publish_entity_create), + JupiterApiResource.build( + "load-by-external-id", + JupiterApiGatewayMethod.post( + publish_entity_load_by_external_id + ), + ), + JupiterApiResource.build( + ":ref_id", + JupiterApiResource.build( + "activate", + JupiterApiGatewayMethod.post(publish_entity_activate), + ), + JupiterApiResource.build( + "to-draft", + JupiterApiGatewayMethod.post(publish_entity_to_draft), + ), + ), + ), + ), # Time Events JupiterApiResource.build( "time-events", diff --git a/src/cli/jupiter/cli/command/exceptions.py b/src/cli/jupiter/cli/command/exceptions.py index ff1b42296..b1bdcf0fd 100644 --- a/src/cli/jupiter/cli/command/exceptions.py +++ b/src/cli/jupiter/cli/command/exceptions.py @@ -11,6 +11,10 @@ BigPlanMilestoneAlreadyExistsForDateError, ) from jupiter.core.common.sub.contacts.sub.contact.root import ContactAlreadyExistsError +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -155,6 +159,26 @@ def handle(self, console: Console, exception: ContactAlreadyExistsError) -> None sys.exit(1) +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + def handle(self, console: Console, exception: EntityIsAlreadyActiveError) -> None: + """Handle entity is already active errors.""" + print(str(exception)) + sys.exit(1) + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" + + def handle(self, console: Console, exception: EntityIsAlreadyDraftError) -> None: + """Handle entity is already draft errors.""" + print(str(exception)) + sys.exit(1) + + class UserNotFoundHandler(JupiterExceptionHandler[UserNotFoundError]): """Handle user not found errors.""" diff --git a/src/core/jupiter/core/application/use_case/init.py b/src/core/jupiter/core/application/use_case/init.py index 8f0a9e033..7b20f7027 100644 --- a/src/core/jupiter/core/application/use_case/init.py +++ b/src/core/jupiter/core/application/use_case/init.py @@ -21,6 +21,7 @@ ) from jupiter.core.common.sub.notes.collection import NoteCollection from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.root import PublishDomain from jupiter.core.common.sub.tags.root import TagDomain from jupiter.core.common.sub.time_events.domain import TimeEventDomain from jupiter.core.common.timezone import Timezone @@ -552,6 +553,14 @@ async def _execute( new_search_domain ) + new_publish_domain = PublishDomain.new_publish_domain( + ctx=context.domain_context, + workspace_ref_id=new_workspace.ref_id, + ) + new_publish_domain = await uow.get_for(PublishDomain).create( + new_publish_domain + ) + new_user_workspace_link = UserWorkspaceLink.new_user_workspace_link( ctx=context.domain_context, user_ref_id=new_user.ref_id, diff --git a/src/core/jupiter/core/application/use_case/init_create_workspace.py b/src/core/jupiter/core/application/use_case/init_create_workspace.py index 3fb64810b..a4e6a7138 100644 --- a/src/core/jupiter/core/application/use_case/init_create_workspace.py +++ b/src/core/jupiter/core/application/use_case/init_create_workspace.py @@ -19,6 +19,7 @@ ) from jupiter.core.common.sub.notes.collection import NoteCollection from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.root import PublishDomain from jupiter.core.common.sub.tags.root import TagDomain from jupiter.core.common.sub.time_events.domain import TimeEventDomain from jupiter.core.common.timezone import Timezone @@ -490,6 +491,14 @@ async def _execute( new_search_domain ) + new_publish_domain = PublishDomain.new_publish_domain( + ctx=context.domain_context, + workspace_ref_id=new_workspace.ref_id, + ) + new_publish_domain = await uow.get_for(PublishDomain).create( + new_publish_domain + ) + new_user_workspace_link = UserWorkspaceLink.new_user_workspace_link( ctx=context.domain_context, user_ref_id=args.user_id, diff --git a/src/core/jupiter/core/common/sub/publish/__init__.py b/src/core/jupiter/core/common/sub/publish/__init__.py new file mode 100644 index 000000000..51822d919 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/__init__.py @@ -0,0 +1,3 @@ +"""Publish entities.""" + +SLICE_TAG = "Publish" diff --git a/src/core/jupiter/core/common/sub/publish/impl/__init__.py b/src/core/jupiter/core/common/sub/publish/impl/__init__.py new file mode 100644 index 000000000..b9f72ad34 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/impl/__init__.py @@ -0,0 +1 @@ +"""Publish infra implementations.""" diff --git a/src/core/jupiter/core/common/sub/publish/impl/postgres_repository.py b/src/core/jupiter/core/common/sub/publish/impl/postgres_repository.py new file mode 100644 index 000000000..ac7e4b3e9 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/impl/postgres_repository.py @@ -0,0 +1,53 @@ +"""PostgreSQL implementation of publish infra classes.""" + +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityAlreadyExistsError, + PublishEntityRepository, +) +from jupiter.framework.realm.realm import RealmCodecRegistry +from jupiter.framework.storage.postgres.repository import PostgresLeafEntityRepository +from jupiter.framework.storage.repository import EntityNotFoundError +from sqlalchemy import MetaData, select +from sqlalchemy.ext.asyncio import AsyncConnection + + +class PostgresPublishEntityRepository( + PostgresLeafEntityRepository[PublishEntity], + PublishEntityRepository, +): + """PostgreSQL implementation of the publish entity repository.""" + + def __init__( + self, + realm_codec_registry: RealmCodecRegistry, + connection: AsyncConnection, + metadata: MetaData, + ) -> None: + """Constructor.""" + super().__init__( + realm_codec_registry, + connection, + metadata, + already_exists_err_cls=PublishEntityAlreadyExistsError, + ) + + async def load_by_external_id( + self, + external_id: PublishExternalId, + allow_archived: bool = False, + ) -> PublishEntity: + """Load a publish entity by its external id.""" + query_stmt = select(self._table).where( + self._table.c.external_id == str(external_id) + ) + if not allow_archived: + query_stmt = query_stmt.where(self._table.c.archived.is_(False)) + + result = (await self._connection.execute(query_stmt)).first() + if result is None: + raise EntityNotFoundError( + f"Publish entity with external id {external_id} does not exist" + ) + return self._row_to_entity(result) diff --git a/src/core/jupiter/core/common/sub/publish/impl/sqlite_repository.py b/src/core/jupiter/core/common/sub/publish/impl/sqlite_repository.py new file mode 100644 index 000000000..d714985d9 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/impl/sqlite_repository.py @@ -0,0 +1,53 @@ +"""SQLite implementation of publish infra classes.""" + +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityAlreadyExistsError, + PublishEntityRepository, +) +from jupiter.framework.realm.realm import RealmCodecRegistry +from jupiter.framework.storage.repository import EntityNotFoundError +from jupiter.framework.storage.sqlite.repository import SqliteLeafEntityRepository +from sqlalchemy import MetaData, select +from sqlalchemy.ext.asyncio import AsyncConnection + + +class SqlitePublishEntityRepository( + SqliteLeafEntityRepository[PublishEntity], + PublishEntityRepository, +): + """SQLite implementation of the publish entity repository.""" + + def __init__( + self, + realm_codec_registry: RealmCodecRegistry, + connection: AsyncConnection, + metadata: MetaData, + ) -> None: + """Constructor.""" + super().__init__( + realm_codec_registry, + connection, + metadata, + already_exists_err_cls=PublishEntityAlreadyExistsError, + ) + + async def load_by_external_id( + self, + external_id: PublishExternalId, + allow_archived: bool = False, + ) -> PublishEntity: + """Load a publish entity by its external id.""" + query_stmt = select(self._table).where( + self._table.c.external_id == str(external_id) + ) + if not allow_archived: + query_stmt = query_stmt.where(self._table.c.archived.is_(False)) + + result = (await self._connection.execute(query_stmt)).first() + if result is None: + raise EntityNotFoundError( + f"Publish entity with external id {external_id} does not exist" + ) + return self._row_to_entity(result) diff --git a/src/core/jupiter/core/common/sub/publish/root.py b/src/core/jupiter/core/common/sub/publish/root.py new file mode 100644 index 000000000..7770d9984 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/root.py @@ -0,0 +1,29 @@ +"""Publish domain trunk entity.""" + +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.context import DomainContext +from jupiter.framework.entity import ( + ContainsMany, + IsRefId, + ParentLink, + TrunkEntity, + entity, +) + + +@entity("Workspace") +class PublishDomain(TrunkEntity): + """Publish trunk entity.""" + + workspace: ParentLink + + entities = ContainsMany(PublishEntity, publish_domain_ref_id=IsRefId()) + + @staticmethod + def new_publish_domain( + ctx: DomainContext, + workspace_ref_id: EntityId, + ) -> "PublishDomain": + """Create a publish domain.""" + return PublishDomain._create(ctx, workspace=ParentLink(workspace_ref_id)) diff --git a/src/core/jupiter/core/common/sub/publish/sub/__init__.py b/src/core/jupiter/core/common/sub/publish/sub/__init__.py new file mode 100644 index 000000000..72accc76e --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/__init__.py @@ -0,0 +1 @@ +"""Publish subentities.""" diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/__init__.py b/src/core/jupiter/core/common/sub/publish/sub/entity/__init__.py new file mode 100644 index 000000000..b20d19f95 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/__init__.py @@ -0,0 +1 @@ +"""Publish entity.""" diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/external_id.py b/src/core/jupiter/core/common/sub/publish/sub/entity/external_id.py new file mode 100644 index 000000000..f7cbfc390 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/external_id.py @@ -0,0 +1,54 @@ +"""External id for publish entities.""" + +import uuid + +from jupiter.framework.errors import InputValidationError +from jupiter.framework.realm.standard import ( + PrimitiveAtomicValueDatabaseDecoder, + PrimitiveAtomicValueDatabaseEncoder, +) +from jupiter.framework.value import AtomicValue, hashable_value + + +@hashable_value +class PublishExternalId(AtomicValue[str]): + """A GUID external id for a publish entity.""" + + the_id: str + + def _validate(self) -> None: + try: + uuid.UUID(self.the_id, version=4) + except (ValueError, TypeError) as err: + raise InputValidationError( + "Publish external id must be a valid UUID v4" + ) from err + + @staticmethod + def new_external_id() -> "PublishExternalId": + """Construct a new external id.""" + return PublishExternalId(str(uuid.uuid4())) + + def __str__(self) -> str: + """The string representation.""" + return self.the_id + + +class PublishExternalIdDatabaseEncoder( + PrimitiveAtomicValueDatabaseEncoder[PublishExternalId] +): + """Encode to a database primitive.""" + + def to_primitive(self, value: PublishExternalId) -> str: + """Encode to a database primitive.""" + return value.the_id + + +class PublishExternalIdDatabaseDecoder( + PrimitiveAtomicValueDatabaseDecoder[PublishExternalId] +): + """Decode from a database primitive.""" + + def from_raw_str(self, primitive: str) -> PublishExternalId: + """Decode from a raw string.""" + return PublishExternalId(primitive) diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/external_id.test.py b/src/core/jupiter/core/common/sub/publish/sub/entity/external_id.test.py new file mode 100644 index 000000000..cd6fa8484 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/external_id.test.py @@ -0,0 +1,24 @@ +"""Tests for publish external id.""" + +import uuid + +import pytest +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.framework.errors import InputValidationError + + +def test_new_external_id_is_valid_uuid_v4() -> None: + external_id = PublishExternalId.new_external_id() + parsed = uuid.UUID(str(external_id), version=4) + assert parsed.version == 4 + + +def test_construction_accepts_valid_uuid_v4() -> None: + value = str(uuid.uuid4()) + external_id = PublishExternalId(value) + assert str(external_id) == value + + +def test_construction_rejects_invalid_uuid() -> None: + with pytest.raises(InputValidationError): + PublishExternalId("not-a-uuid") diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/name.py b/src/core/jupiter/core/common/sub/publish/sub/entity/name.py new file mode 100644 index 000000000..ebff401f1 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/name.py @@ -0,0 +1,9 @@ +"""The name of a publish entity.""" + +from jupiter.framework.base.entity_name import EntityName +from jupiter.framework.value import hashable_value + + +@hashable_value +class PublishEntityName(EntityName): + """The name of a publish entity.""" diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py new file mode 100644 index 000000000..ac3e67a7e --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -0,0 +1,129 @@ +"""A publish entity.""" + +import abc +from typing import Final + +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.name import PublishEntityName +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.context import DomainContext +from jupiter.framework.entity import ( + LeafEntity, + ParentLink, + create_entity_action, + entity, + update_entity_action, +) +from jupiter.framework.errors import InputValidationError +from jupiter.framework.storage.repository import ( + EntityAlreadyExistsError, + LeafEntityRepository, +) + +# Allowed ``NamedEntityTag`` values for shareable :class:`PublishEntity` targets. +ALLOWED_PUBLISH_ENTITY_TYPES: Final[frozenset[str]] = frozenset( + { + NamedEntityTag.WORKING_MEM.value, + NamedEntityTag.TIME_PLAN.value, + NamedEntityTag.SCHEDULE_STREAM.value, + NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, + NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, + NamedEntityTag.HABIT.value, + NamedEntityTag.CHORE.value, + NamedEntityTag.BIG_PLAN.value, + NamedEntityTag.DOC.value, + NamedEntityTag.DIR.value, + NamedEntityTag.JOURNAL.value, + NamedEntityTag.CHAPTER.value, + NamedEntityTag.GOAL.value, + NamedEntityTag.MILESTONE.value, + NamedEntityTag.VISION.value, + NamedEntityTag.VACATION.value, + NamedEntityTag.ASPECT.value, + NamedEntityTag.SMART_LIST.value, + NamedEntityTag.SMART_LIST_ITEM.value, + NamedEntityTag.METRIC.value, + NamedEntityTag.METRIC_ENTRY.value, + NamedEntityTag.PERSON.value, + } +) + + +class PublishEntityAlreadyExistsError(EntityAlreadyExistsError): + """Error raised when a publish entity already exists for an entity.""" + + +class EntityIsAlreadyActiveError(Exception): + """Error raised when trying to activate an already active entity.""" + + +class EntityIsAlreadyDraftError(Exception): + """Error raised when trying to move an already draft entity to draft.""" + + +@entity("PublishDomain") +class PublishEntity(LeafEntity): + """A publish entity.""" + + publish_domain: ParentLink + name: PublishEntityName + entity_type: str + entity_ref_id: EntityId + external_id: PublishExternalId + status: PublishEntityStatus + + @staticmethod + @create_entity_action + def new_publish_entity( + ctx: DomainContext, + publish_domain_ref_id: EntityId, + name: PublishEntityName, + entity_type: str, + entity_ref_id: EntityId, + ) -> "PublishEntity": + """Create a publish entity.""" + if entity_type not in ALLOWED_PUBLISH_ENTITY_TYPES: + raise InputValidationError( + f"Invalid publish entity type: {entity_type!r}", + ) + return PublishEntity._create( + ctx, + publish_domain=ParentLink(publish_domain_ref_id), + name=name, + entity_type=entity_type, + entity_ref_id=entity_ref_id, + external_id=PublishExternalId.new_external_id(), + status=PublishEntityStatus.DRAFT, + ) + + @update_entity_action + def activate(self, ctx: DomainContext) -> "PublishEntity": + """Activate the publish entity.""" + if self.status == PublishEntityStatus.ACTIVE: + raise EntityIsAlreadyActiveError( + "The publish entity is already active.", + ) + return self._new_version(ctx, status=PublishEntityStatus.ACTIVE) + + @update_entity_action + def to_draft(self, ctx: DomainContext) -> "PublishEntity": + """Move the publish entity back to draft.""" + if self.status == PublishEntityStatus.DRAFT: + raise EntityIsAlreadyDraftError( + "The publish entity is already a draft.", + ) + return self._new_version(ctx, status=PublishEntityStatus.DRAFT) + + +class PublishEntityRepository(LeafEntityRepository[PublishEntity], abc.ABC): + """A repository for publish entities.""" + + @abc.abstractmethod + async def load_by_external_id( + self, + external_id: PublishExternalId, + allow_archived: bool = False, + ) -> PublishEntity: + """Load a publish entity by its external id.""" diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.test.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.test.py new file mode 100644 index 000000000..84d2c590c --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.test.py @@ -0,0 +1,39 @@ +"""Tests for publish entity.""" + +from jupiter.core.common.sub.publish.sub.entity.root import ( + ALLOWED_PUBLISH_ENTITY_TYPES, +) +from jupiter.core.named_entity_tag import NamedEntityTag + + +def test_allowed_publish_entity_types_matches_shareable_named_entity_tags() -> None: + assert ALLOWED_PUBLISH_ENTITY_TYPES == frozenset( + { + NamedEntityTag.WORKING_MEM.value, + NamedEntityTag.TIME_PLAN.value, + NamedEntityTag.SCHEDULE_STREAM.value, + NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, + NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, + NamedEntityTag.HABIT.value, + NamedEntityTag.CHORE.value, + NamedEntityTag.BIG_PLAN.value, + NamedEntityTag.DOC.value, + NamedEntityTag.DIR.value, + NamedEntityTag.JOURNAL.value, + NamedEntityTag.CHAPTER.value, + NamedEntityTag.GOAL.value, + NamedEntityTag.MILESTONE.value, + NamedEntityTag.VISION.value, + NamedEntityTag.VACATION.value, + NamedEntityTag.ASPECT.value, + NamedEntityTag.SMART_LIST.value, + NamedEntityTag.SMART_LIST_ITEM.value, + NamedEntityTag.METRIC.value, + NamedEntityTag.METRIC_ENTRY.value, + NamedEntityTag.PERSON.value, + } + ) + + +def test_todo_task_is_not_an_allowed_publish_entity_type() -> None: + assert NamedEntityTag.TODO_TASK.value not in ALLOWED_PUBLISH_ENTITY_TYPES diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/status.py b/src/core/jupiter/core/common/sub/publish/sub/entity/status.py new file mode 100644 index 000000000..8ab95b379 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/status.py @@ -0,0 +1,11 @@ +"""Status for publish entities.""" + +from jupiter.framework.value import EnumValue, enum_value + + +@enum_value +class PublishEntityStatus(EnumValue): + """The status of a publish entity.""" + + DRAFT = "draft" + ACTIVE = "active" diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/__init__.py b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/__init__.py new file mode 100644 index 000000000..d36a29c55 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/__init__.py @@ -0,0 +1 @@ +"""Publish entity use cases.""" diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/activate.py b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/activate.py new file mode 100644 index 000000000..012a22dea --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/activate.py @@ -0,0 +1,50 @@ +"""Use case for activating a publish entity.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity +from jupiter.core.config import ( + JupiterLoggedInMutationContext, + JupiterTransactionalLoggedInMutationUseCase, +) +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.errors import InputValidationError +from jupiter.framework.progress_reporter.reporter import ProgressReporter +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case import mutation_use_case +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class PublishEntityActivateArgs(UseCaseArgsBase): + """PublishEntityActivate args.""" + + ref_id: EntityId + + +@mutation_use_case() +class PublishEntityActivateUseCase( + JupiterTransactionalLoggedInMutationUseCase[PublishEntityActivateArgs, None] +): + """Use case for activating a publish entity.""" + + async def _perform_transactional_mutation( + self, + uow: DomainUnitOfWork, + progress_reporter: ProgressReporter, + context: JupiterLoggedInMutationContext, + args: PublishEntityActivateArgs, + ) -> None: + """Execute the command's action.""" + publish_domain = await uow.get_for(PublishDomain).load_by_parent( + context.workspace.ref_id + ) + publish_entity = await uow.get_for(PublishEntity).load_by_id(args.ref_id) + + if publish_entity.parent_ref_id != publish_domain.ref_id: + raise InputValidationError( + "The publish entity does not belong to this workspace." + ) + + publish_entity = publish_entity.activate(ctx=context.domain_context) + await uow.get_for(PublishEntity).save(publish_entity) + await progress_reporter.mark_updated(publish_entity) diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/create.py b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/create.py new file mode 100644 index 000000000..e3c50450f --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/create.py @@ -0,0 +1,67 @@ +"""Use case for creating a publish entity.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.name import PublishEntityName +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity +from jupiter.core.config import ( + JupiterLoggedInMutationContext, + JupiterTransactionalLoggedInMutationUseCase, +) +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.progress_reporter.reporter import ProgressReporter +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case import mutation_use_case +from jupiter.framework.use_case_io import ( + UseCaseArgsBase, + UseCaseResultBase, + use_case_args, + use_case_result, +) + + +@use_case_args +class PublishEntityCreateArgs(UseCaseArgsBase): + """PublishEntityCreate args.""" + + name: PublishEntityName + entity_type: str + entity_ref_id: EntityId + + +@use_case_result +class PublishEntityCreateResult(UseCaseResultBase): + """PublishEntityCreate result.""" + + new_publish_entity: PublishEntity + + +@mutation_use_case() +class PublishEntityCreateUseCase( + JupiterTransactionalLoggedInMutationUseCase[ + PublishEntityCreateArgs, PublishEntityCreateResult + ] +): + """Use case for creating a publish entity.""" + + async def _perform_transactional_mutation( + self, + uow: DomainUnitOfWork, + progress_reporter: ProgressReporter, + context: JupiterLoggedInMutationContext, + args: PublishEntityCreateArgs, + ) -> PublishEntityCreateResult: + """Execute the command's action.""" + workspace = context.workspace + publish_domain = await uow.get_for(PublishDomain).load_by_parent( + workspace.ref_id + ) + + new_publish_entity = PublishEntity.new_publish_entity( + ctx=context.domain_context, + publish_domain_ref_id=publish_domain.ref_id, + name=args.name, + entity_type=args.entity_type, + entity_ref_id=args.entity_ref_id, + ) + new_publish_entity = await uow.get_for(PublishEntity).create(new_publish_entity) + return PublishEntityCreateResult(new_publish_entity=new_publish_entity) diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load_by_external_id.py b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load_by_external_id.py new file mode 100644 index 000000000..ef054f8e9 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load_by_external_id.py @@ -0,0 +1,59 @@ +"""Guest readonly use case for loading a publish entity by external id.""" + +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import ( + UseCaseArgsBase, + UseCaseResultBase, + use_case_args, + use_case_result, +) + + +@use_case_args +class PublishEntityLoadByExternalIdArgs(UseCaseArgsBase): + """PublishEntityLoadByExternalId args.""" + + external_id: PublishExternalId + + +@use_case_result +class PublishEntityLoadByExternalIdResult(UseCaseResultBase): + """PublishEntityLoadByExternalId result.""" + + publish_entity: PublishEntity + + +class PublishEntityLoadByExternalIdUseCase( + JupiterGuestReadonlyUseCase[ + PublishEntityLoadByExternalIdArgs, PublishEntityLoadByExternalIdResult + ] +): + """Load a publish entity by its external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: PublishEntityLoadByExternalIdArgs, + ) -> PublishEntityLoadByExternalIdResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + return PublishEntityLoadByExternalIdResult(publish_entity=publish_entity) diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/to_draft.py b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/to_draft.py new file mode 100644 index 000000000..8e3dd3eb0 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/to_draft.py @@ -0,0 +1,50 @@ +"""Use case for moving a publish entity back to draft.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity +from jupiter.core.config import ( + JupiterLoggedInMutationContext, + JupiterTransactionalLoggedInMutationUseCase, +) +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.errors import InputValidationError +from jupiter.framework.progress_reporter.reporter import ProgressReporter +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case import mutation_use_case +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class PublishEntityToDraftArgs(UseCaseArgsBase): + """PublishEntityToDraft args.""" + + ref_id: EntityId + + +@mutation_use_case() +class PublishEntityToDraftUseCase( + JupiterTransactionalLoggedInMutationUseCase[PublishEntityToDraftArgs, None] +): + """Use case for moving a publish entity back to draft.""" + + async def _perform_transactional_mutation( + self, + uow: DomainUnitOfWork, + progress_reporter: ProgressReporter, + context: JupiterLoggedInMutationContext, + args: PublishEntityToDraftArgs, + ) -> None: + """Execute the command's action.""" + publish_domain = await uow.get_for(PublishDomain).load_by_parent( + context.workspace.ref_id + ) + publish_entity = await uow.get_for(PublishEntity).load_by_id(args.ref_id) + + if publish_entity.parent_ref_id != publish_domain.ref_id: + raise InputValidationError( + "The publish entity does not belong to this workspace." + ) + + publish_entity = publish_entity.to_draft(ctx=context.domain_context) + await uow.get_for(PublishEntity).save(publish_entity) + await progress_reporter.mark_updated(publish_entity) diff --git a/src/core/jupiter/core/workspaces/root.py b/src/core/jupiter/core/workspaces/root.py index c18162ce0..8d2b0dd6e 100644 --- a/src/core/jupiter/core/workspaces/root.py +++ b/src/core/jupiter/core/workspaces/root.py @@ -26,6 +26,7 @@ WORKING_MEM_CLEANUP, ) from jupiter.core.common.sub.notes.collection import NoteCollection +from jupiter.core.common.sub.publish.root import PublishDomain from jupiter.core.common.sub.tags.root import TagDomain from jupiter.core.common.sub.time_events.domain import TimeEventDomain from jupiter.core.docs.root import DocCollection @@ -114,6 +115,7 @@ class Workspace(RootEntity): gen_log = ContainsOne(GenLog, workspace_ref_id=IsRefId()) stats_log = ContainsOne(StatsLog, workspace_ref_id=IsRefId()) search_domain = ContainsOne(SearchDomain, workspace_ref_id=IsRefId()) + publish_domain = ContainsOne(PublishDomain, workspace_ref_id=IsRefId()) @staticmethod @create_entity_action diff --git a/src/core/migrations/postgres/versions/2026_06_07_00_29_publish_domain.py b/src/core/migrations/postgres/versions/2026_06_07_00_29_publish_domain.py new file mode 100644 index 000000000..d97bf8ab6 --- /dev/null +++ b/src/core/migrations/postgres/versions/2026_06_07_00_29_publish_domain.py @@ -0,0 +1,119 @@ +"""publish domain + +Revision ID: bd0eef3634c2 +Revises: c3cc27543077 +Create Date: 2026-06-07 00:29:19.794553 + +""" + +from alembic import op +from sqlalchemy import text + +revision = "bd0eef3634c2" +down_revision = "c3cc27543077" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE publish_domain ( + ref_id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + version INTEGER NOT NULL, + archived BOOLEAN NOT NULL, + archival_reason VARCHAR, + created_time TIMESTAMP WITH TIME ZONE NOT NULL, + last_modified_time TIMESTAMP WITH TIME ZONE NOT NULL, + archived_time TIMESTAMP WITH TIME ZONE, + workspace_ref_id INTEGER NOT NULL, + PRIMARY KEY (ref_id), + FOREIGN KEY (workspace_ref_id) REFERENCES workspace (ref_id) + ) + """ + ) + + op.execute( + """ + CREATE UNIQUE INDEX ix_publish_domain_workspace_ref_id + ON publish_domain (workspace_ref_id) + """ + ) + + conn = op.get_bind() + conn.execute( + text( + """ + INSERT INTO publish_domain ( + version, + archived, + archival_reason, + created_time, + last_modified_time, + archived_time, + workspace_ref_id + ) + SELECT + version, + archived, + archival_reason, + created_time, + last_modified_time, + archived_time, + ref_id + FROM workspace + """ + ) + ) + + op.execute( + """ + CREATE TABLE publish_entity ( + ref_id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL, + version INTEGER NOT NULL, + archived BOOLEAN NOT NULL, + archival_reason VARCHAR, + created_time TIMESTAMP WITH TIME ZONE NOT NULL, + last_modified_time TIMESTAMP WITH TIME ZONE NOT NULL, + archived_time TIMESTAMP WITH TIME ZONE, + publish_domain_ref_id INTEGER NOT NULL, + name VARCHAR(255) NOT NULL, + entity_type VARCHAR(255) NOT NULL, + entity_ref_id INTEGER NOT NULL, + external_id VARCHAR(64) NOT NULL, + status VARCHAR(32) NOT NULL, + PRIMARY KEY (ref_id), + FOREIGN KEY (publish_domain_ref_id) REFERENCES publish_domain (ref_id) + ) + """ + ) + + op.execute( + """ + CREATE INDEX ix_publish_entity_publish_domain_ref_id + ON publish_entity (publish_domain_ref_id) + """ + ) + + op.execute( + """ + CREATE INDEX ix_publish_entity_external_id + ON publish_entity (external_id) + """ + ) + + op.execute( + """ + CREATE UNIQUE INDEX ix_publish_entity_entity_type_entity_ref_id + ON publish_entity (entity_type, entity_ref_id) + """ + ) + + +def downgrade() -> None: + op.execute("DROP INDEX ix_publish_entity_entity_type_entity_ref_id") + op.execute("DROP INDEX ix_publish_entity_external_id") + op.execute("DROP INDEX ix_publish_entity_publish_domain_ref_id") + op.execute("DROP TABLE publish_entity") + op.execute("DROP INDEX ix_publish_domain_workspace_ref_id") + op.execute("DROP TABLE publish_domain") diff --git a/src/core/migrations/sqlite/versions/2026_06_07_00_29_publish_domain.py b/src/core/migrations/sqlite/versions/2026_06_07_00_29_publish_domain.py new file mode 100644 index 000000000..213102d3f --- /dev/null +++ b/src/core/migrations/sqlite/versions/2026_06_07_00_29_publish_domain.py @@ -0,0 +1,119 @@ +"""publish domain + +Revision ID: bd0eef3634c2 +Revises: c3cc27543077 +Create Date: 2026-06-07 00:29:19.794553 + +""" + +from alembic import op +from sqlalchemy import text + +revision = "bd0eef3634c2" +down_revision = "c3cc27543077" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE publish_domain ( + ref_id INTEGER NOT NULL, + version INTEGER NOT NULL, + archived BOOLEAN NOT NULL, + archival_reason VARCHAR, + created_time DATETIME NOT NULL, + last_modified_time DATETIME NOT NULL, + archived_time DATETIME, + workspace_ref_id INTEGER NOT NULL, + PRIMARY KEY (ref_id), + FOREIGN KEY (workspace_ref_id) REFERENCES workspace (ref_id) + ) + """ + ) + + op.execute( + """ + CREATE UNIQUE INDEX ix_publish_domain_workspace_ref_id + ON publish_domain (workspace_ref_id) + """ + ) + + conn = op.get_bind() + conn.execute( + text( + """ + INSERT INTO publish_domain ( + version, + archived, + archival_reason, + created_time, + last_modified_time, + archived_time, + workspace_ref_id + ) + SELECT + version, + archived, + archival_reason, + created_time, + last_modified_time, + archived_time, + ref_id + FROM workspace + """ + ) + ) + + op.execute( + """ + CREATE TABLE publish_entity ( + ref_id INTEGER NOT NULL, + version INTEGER NOT NULL, + archived BOOLEAN NOT NULL, + archival_reason VARCHAR, + created_time DATETIME NOT NULL, + last_modified_time DATETIME NOT NULL, + archived_time DATETIME, + publish_domain_ref_id INTEGER NOT NULL, + name VARCHAR(255) NOT NULL, + entity_type VARCHAR(255) NOT NULL, + entity_ref_id INTEGER NOT NULL, + external_id VARCHAR(64) NOT NULL, + status VARCHAR(32) NOT NULL, + PRIMARY KEY (ref_id), + FOREIGN KEY (publish_domain_ref_id) REFERENCES publish_domain (ref_id) + ) + """ + ) + + op.execute( + """ + CREATE INDEX ix_publish_entity_publish_domain_ref_id + ON publish_entity (publish_domain_ref_id) + """ + ) + + op.execute( + """ + CREATE INDEX ix_publish_entity_external_id + ON publish_entity (external_id) + """ + ) + + op.execute( + """ + CREATE UNIQUE INDEX ix_publish_entity_entity_type_entity_ref_id + ON publish_entity (entity_type, entity_ref_id) + """ + ) + + +def downgrade() -> None: + op.execute("DROP INDEX ix_publish_entity_entity_type_entity_ref_id") + op.execute("DROP INDEX ix_publish_entity_external_id") + op.execute("DROP INDEX ix_publish_entity_publish_domain_ref_id") + op.execute("DROP TABLE publish_entity") + op.execute("DROP INDEX ix_publish_domain_workspace_ref_id") + op.execute("DROP TABLE publish_domain") diff --git a/src/mcp/jupiter/mcp/jupiter.py b/src/mcp/jupiter/mcp/jupiter.py index 0d5b37d45..996467fba 100644 --- a/src/mcp/jupiter/mcp/jupiter.py +++ b/src/mcp/jupiter/mcp/jupiter.py @@ -543,6 +543,20 @@ asyncio_detailed as smart_list_update, ) +# --- Publish API --- +from jupiter_webapi_client.api.publish.publish_entity_activate import ( + asyncio_detailed as publish_entity_activate, +) +from jupiter_webapi_client.api.publish.publish_entity_create import ( + asyncio_detailed as publish_entity_create, +) +from jupiter_webapi_client.api.publish.publish_entity_load_by_external_id import ( + asyncio_detailed as publish_entity_load_by_external_id, +) +from jupiter_webapi_client.api.publish.publish_entity_to_draft import ( + asyncio_detailed as publish_entity_to_draft, +) + # --- Tags API --- from jupiter_webapi_client.api.tags.tag_archive import ( asyncio_detailed as tag_archive, @@ -1246,6 +1260,27 @@ async def main() -> None: JupiterMcpTool.tool( "upsert-tag-link", "Create or update a tag link", tag_link_upsert ), + # --- Publish --- + JupiterMcpTool.tool( + "create-publish-entity", + "Create a publish entity", + publish_entity_create, + ), + JupiterMcpTool.tool( + "activate-publish-entity", + "Activate a publish entity", + publish_entity_activate, + ), + JupiterMcpTool.tool( + "to-draft-publish-entity", + "Move a publish entity back to draft", + publish_entity_to_draft, + ), + JupiterMcpTool.tool( + "load-publish-entity-by-external-id", + "Load a publish entity by external id", + publish_entity_load_by_external_id, + ), # --- Contacts --- JupiterMcpResource.resource("jupiter://contacts", contact_find), JupiterMcpTool.tool("find-contacts", "Find contacts", contact_find), diff --git a/src/webapi/clear-abandoned-users-do-all/jupiter_webapi_clear_abandoned_users_do_all/exceptions.py b/src/webapi/clear-abandoned-users-do-all/jupiter_webapi_clear_abandoned_users_do_all/exceptions.py index 6a010a483..60f594c6e 100644 --- a/src/webapi/clear-abandoned-users-do-all/jupiter_webapi_clear_abandoned_users_do_all/exceptions.py +++ b/src/webapi/clear-abandoned-users-do-all/jupiter_webapi_clear_abandoned_users_do_all/exceptions.py @@ -12,6 +12,10 @@ ContactAlreadyExistsError, ContactInSignificantUseError, ) +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -91,3 +95,13 @@ class ContactInSignificantUseHandler( class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" + + +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" diff --git a/src/webapi/crm-backfill-do-all/jupiter_webapi_crm_backfill_do_all/exceptions.py b/src/webapi/crm-backfill-do-all/jupiter_webapi_crm_backfill_do_all/exceptions.py index 30139aeec..caaa3751e 100644 --- a/src/webapi/crm-backfill-do-all/jupiter_webapi_crm_backfill_do_all/exceptions.py +++ b/src/webapi/crm-backfill-do-all/jupiter_webapi_crm_backfill_do_all/exceptions.py @@ -12,6 +12,10 @@ ContactAlreadyExistsError, ContactInSignificantUseError, ) +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -91,3 +95,13 @@ class ContactInSignificantUseHandler( class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" + + +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" diff --git a/src/webapi/gc-do-all/jupiter_webapi_gc_do_all/exceptions.py b/src/webapi/gc-do-all/jupiter_webapi_gc_do_all/exceptions.py index 1c2038986..2a3c6e239 100644 --- a/src/webapi/gc-do-all/jupiter_webapi_gc_do_all/exceptions.py +++ b/src/webapi/gc-do-all/jupiter_webapi_gc_do_all/exceptions.py @@ -12,6 +12,10 @@ ContactAlreadyExistsError, ContactInSignificantUseError, ) +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -91,3 +95,13 @@ class ContactInSignificantUseHandler( class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" + + +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" diff --git a/src/webapi/gen-do-all/jupiter_webapi_gen_do_all/exceptions.py b/src/webapi/gen-do-all/jupiter_webapi_gen_do_all/exceptions.py index 726ca7d53..d3c7096fb 100644 --- a/src/webapi/gen-do-all/jupiter_webapi_gen_do_all/exceptions.py +++ b/src/webapi/gen-do-all/jupiter_webapi_gen_do_all/exceptions.py @@ -12,6 +12,10 @@ ContactAlreadyExistsError, ContactInSignificantUseError, ) +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -91,3 +95,13 @@ class ContactInSignificantUseHandler( class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" + + +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" diff --git a/src/webapi/schedule-external-sync-do-all/jupiter_webapi_schedule_external_sync_do_all/exceptions.py b/src/webapi/schedule-external-sync-do-all/jupiter_webapi_schedule_external_sync_do_all/exceptions.py index 8d127bb3f..21cedc034 100644 --- a/src/webapi/schedule-external-sync-do-all/jupiter_webapi_schedule_external_sync_do_all/exceptions.py +++ b/src/webapi/schedule-external-sync-do-all/jupiter_webapi_schedule_external_sync_do_all/exceptions.py @@ -12,6 +12,10 @@ ContactAlreadyExistsError, ContactInSignificantUseError, ) +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -91,3 +95,13 @@ class ContactInSignificantUseHandler( class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" + + +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" diff --git a/src/webapi/search-index-backfill-do-all/jupiter_webapi_search_index_backfill_do_all/exceptions.py b/src/webapi/search-index-backfill-do-all/jupiter_webapi_search_index_backfill_do_all/exceptions.py index f93175175..ea5aea219 100644 --- a/src/webapi/search-index-backfill-do-all/jupiter_webapi_search_index_backfill_do_all/exceptions.py +++ b/src/webapi/search-index-backfill-do-all/jupiter_webapi_search_index_backfill_do_all/exceptions.py @@ -12,6 +12,10 @@ ContactAlreadyExistsError, ContactInSignificantUseError, ) +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -91,3 +95,13 @@ class ContactInSignificantUseHandler( class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" + + +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" diff --git a/src/webapi/search-mutation-log-drain-do-all/jupiter_webapi_search_mutation_log_drain_do_all/exceptions.py b/src/webapi/search-mutation-log-drain-do-all/jupiter_webapi_search_mutation_log_drain_do_all/exceptions.py index 588230973..4a5143469 100644 --- a/src/webapi/search-mutation-log-drain-do-all/jupiter_webapi_search_mutation_log_drain_do_all/exceptions.py +++ b/src/webapi/search-mutation-log-drain-do-all/jupiter_webapi_search_mutation_log_drain_do_all/exceptions.py @@ -12,6 +12,10 @@ ContactAlreadyExistsError, ContactInSignificantUseError, ) +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -93,3 +97,13 @@ class ContactInSignificantUseHandler( class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" + + +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" diff --git a/src/webapi/search-mutation-requeue-do-all/jupiter_webapi_search_mutation_requeue_do_all/exceptions.py b/src/webapi/search-mutation-requeue-do-all/jupiter_webapi_search_mutation_requeue_do_all/exceptions.py index d7c836f76..9047ce939 100644 --- a/src/webapi/search-mutation-requeue-do-all/jupiter_webapi_search_mutation_requeue_do_all/exceptions.py +++ b/src/webapi/search-mutation-requeue-do-all/jupiter_webapi_search_mutation_requeue_do_all/exceptions.py @@ -12,6 +12,10 @@ ContactAlreadyExistsError, ContactInSignificantUseError, ) +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -93,3 +97,13 @@ class ContactInSignificantUseHandler( class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" + + +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" diff --git a/src/webapi/srv/jupiter/webapi/exceptions.py b/src/webapi/srv/jupiter/webapi/exceptions.py index be0a2e0dc..f2b8ac1a6 100644 --- a/src/webapi/srv/jupiter/webapi/exceptions.py +++ b/src/webapi/srv/jupiter/webapi/exceptions.py @@ -19,6 +19,10 @@ ContactAlreadyExistsError, ContactInSignificantUseError, ) +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -354,6 +358,44 @@ def get_detail(self, exception: TagAlreadyExistsError) -> WebApiError: ) +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + @staticmethod + def get_status_code() -> int: + """Get the status code for the exception.""" + return status.HTTP_409_CONFLICT + + def get_detail(self, exception: EntityIsAlreadyActiveError) -> WebApiError: + """Get the detail for the exception.""" + return WebApiError.validation( + "Entity is already active", + loc=["body"], + msg=str(exception), + error_type="value_error.entityisalreadyactiveerror", + ) + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" + + @staticmethod + def get_status_code() -> int: + """Get the status code for the exception.""" + return status.HTTP_409_CONFLICT + + def get_detail(self, exception: EntityIsAlreadyDraftError) -> WebApiError: + """Get the detail for the exception.""" + return WebApiError.validation( + "Entity is already a draft", + loc=["body"], + msg=str(exception), + error_type="value_error.entityisalreadydrafterror", + ) + + class InvalidEmailAttemptVerificationStateHandler( JupiterExceptionHandler[InvalidEmailAttemptVerificationStateError] ): diff --git a/src/webapi/stats-do-all/jupiter_webapi_stats_do_all/exceptions.py b/src/webapi/stats-do-all/jupiter_webapi_stats_do_all/exceptions.py index a9a3615c0..7b421d0c6 100644 --- a/src/webapi/stats-do-all/jupiter_webapi_stats_do_all/exceptions.py +++ b/src/webapi/stats-do-all/jupiter_webapi_stats_do_all/exceptions.py @@ -12,6 +12,10 @@ ContactAlreadyExistsError, ContactInSignificantUseError, ) +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -91,3 +95,13 @@ class ContactInSignificantUseHandler( class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" + + +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" diff --git a/src/webapi/sync-google-user-data-do-all/jupiter_webapi_sync_google_user_data_do_all/exceptions.py b/src/webapi/sync-google-user-data-do-all/jupiter_webapi_sync_google_user_data_do_all/exceptions.py index 8f871e41d..7812e4f08 100644 --- a/src/webapi/sync-google-user-data-do-all/jupiter_webapi_sync_google_user_data_do_all/exceptions.py +++ b/src/webapi/sync-google-user-data-do-all/jupiter_webapi_sync_google_user_data_do_all/exceptions.py @@ -12,6 +12,10 @@ ContactAlreadyExistsError, ContactInSignificantUseError, ) +from jupiter.core.common.sub.publish.sub.entity.root import ( + EntityIsAlreadyActiveError, + EntityIsAlreadyDraftError, +) from jupiter.core.common.sub.tags.sub.tag.root import TagAlreadyExistsError from jupiter.core.journals.root import ( JournalExistsForDatePeriodCombinationError, @@ -91,3 +95,13 @@ class ContactInSignificantUseHandler( class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" + + +class EntityIsAlreadyActiveHandler( + JupiterExceptionHandler[EntityIsAlreadyActiveError] +): + """Handle entity is already active errors.""" + + +class EntityIsAlreadyDraftHandler(JupiterExceptionHandler[EntityIsAlreadyDraftError]): + """Handle entity is already draft errors.""" From 7c535285dadc4cc7c40479ef47d9a5ba369441ec Mon Sep 17 00:00:00 2001 From: Mike Bestcat Date: Sun, 7 Jun 2026 16:37:58 +0300 Subject: [PATCH 02/21] Work on honeydew --- .../api/todo/todo_task_load_public.py | 212 ++++++++++++++++++ .../jupiter_webapi_client/models/__init__.py | 2 + .../models/publish_entity.py | 20 +- .../models/publish_entity_create_args.py | 28 +-- .../models/todo_task_load_public_args.py | 62 +++++ .../models/todo_task_load_result.py | 33 +++ gen/ts/webapi-client/gen/index.ts | 1 + .../webapi-client/gen/models/PublishEntity.ts | 4 +- .../gen/models/PublishEntityCreateArgs.ts | 7 +- .../gen/models/TodoTaskLoadPublicArgs.ts | 12 + .../gen/models/TodoTaskLoadResult.ts | 2 + .../webapi-client/gen/services/TodoService.ts | 29 +++ itests/api/common/publish.test.py | 32 ++- itests/api/todos.test.py | 82 ++++++- src/api/jupiter/api/jupiter.py | 28 +-- src/cli/jupiter/cli/command/exceptions.py | 4 +- .../sub/publish/components/publish-panel.tsx | 130 +++++++++++ .../sub/publish/impl/postgres_repository.py | 17 ++ .../sub/publish/impl/sqlite_repository.py | 17 ++ .../common/sub/publish/sub/entity/root.py | 37 ++- .../sub/publish/sub/entity/root.test.py | 13 +- .../sub/publish/sub/entity/use_case/create.py | 11 +- .../infra/component/layout/leaf-panel.tsx | 86 +++++-- src/core/jupiter/core/todo/root.py | 4 + src/core/jupiter/core/todo/service/load.py | 147 ++++++++++++ src/core/jupiter/core/todo/use_case/load.py | 128 +---------- .../jupiter/core/todo/use_case/load_public.py | 71 ++++++ .../2026_06_07_00_29_publish_domain.py | 9 +- .../2026_06_07_00_29_publish_domain.py | 9 +- src/mcp/jupiter/mcp/jupiter.py | 28 +-- .../exceptions.py | 4 +- .../exceptions.py | 4 +- .../jupiter_webapi_gc_do_all/exceptions.py | 4 +- .../jupiter_webapi_gen_do_all/exceptions.py | 4 +- .../exceptions.py | 4 +- .../exceptions.py | 4 +- .../exceptions.py | 4 +- .../exceptions.py | 4 +- src/webapi/srv/jupiter/webapi/exceptions.py | 4 +- .../jupiter_webapi_stats_do_all/exceptions.py | 4 +- .../exceptions.py | 4 +- src/webui/app/routes/app/public/published.tsx | 99 ++++++++ .../app/public/published/$externalId.tsx | 62 +++++ .../published/todo-task/$externalId.tsx | 113 ++++++++++ .../app/routes/app/workspace/todos/$id.tsx | 39 ++++ 45 files changed, 1322 insertions(+), 300 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/todo/todo_task_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/todo_task_load_public_args.py create mode 100644 gen/ts/webapi-client/gen/models/TodoTaskLoadPublicArgs.ts create mode 100644 src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx create mode 100644 src/core/jupiter/core/todo/service/load.py create mode 100644 src/core/jupiter/core/todo/use_case/load_public.py create mode 100644 src/webui/app/routes/app/public/published.tsx create mode 100644 src/webui/app/routes/app/public/published/$externalId.tsx create mode 100644 src/webui/app/routes/app/public/published/todo-task/$externalId.tsx diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/todo/todo_task_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/todo/todo_task_load_public.py new file mode 100644 index 000000000..76eb06b65 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/todo/todo_task_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.todo_task_load_public_args import TodoTaskLoadPublicArgs +from ...models.todo_task_load_result import TodoTaskLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: TodoTaskLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/todo-task-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | TodoTaskLoadResult | None: + if response.status_code == 200: + response_200 = TodoTaskLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | TodoTaskLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: TodoTaskLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | TodoTaskLoadResult]: + """Load a published todo task and its dependent entities by publish external id. + + Args: + body (TodoTaskLoadPublicArgs | Unset): TodoTaskLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | TodoTaskLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: TodoTaskLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | TodoTaskLoadResult | None: + """Load a published todo task and its dependent entities by publish external id. + + Args: + body (TodoTaskLoadPublicArgs | Unset): TodoTaskLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | TodoTaskLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: TodoTaskLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | TodoTaskLoadResult]: + """Load a published todo task and its dependent entities by publish external id. + + Args: + body (TodoTaskLoadPublicArgs | Unset): TodoTaskLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | TodoTaskLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: TodoTaskLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | TodoTaskLoadResult | None: + """Load a published todo task and its dependent entities by publish external id. + + Args: + body (TodoTaskLoadPublicArgs | Unset): TodoTaskLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | TodoTaskLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index 014d267d8..fd9ae9ca1 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -914,6 +914,7 @@ from .todo_task_find_result import TodoTaskFindResult from .todo_task_find_result_entry import TodoTaskFindResultEntry from .todo_task_load_args import TodoTaskLoadArgs +from .todo_task_load_public_args import TodoTaskLoadPublicArgs from .todo_task_load_result import TodoTaskLoadResult from .todo_task_remove_args import TodoTaskRemoveArgs from .todo_task_summary import TodoTaskSummary @@ -1900,6 +1901,7 @@ "TodoTaskFindResult", "TodoTaskFindResultEntry", "TodoTaskLoadArgs", + "TodoTaskLoadPublicArgs", "TodoTaskLoadResult", "TodoTaskRemoveArgs", "TodoTaskSummary", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity.py index 5441a8df7..f2c5c382a 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity.py @@ -24,8 +24,7 @@ class PublishEntity: last_modified_time (str): A timestamp in the application. name (str): The name of a publish entity. publish_domain_ref_id (str): - entity_type (str): - entity_ref_id (str): A generic entity id. + owner (str): A reference combining an entity kind, a purpose, and an entity id. external_id (str): A GUID external id for a publish entity. status (PublishEntityStatus): The status of a publish entity. archival_reason (None | str | Unset): @@ -39,8 +38,7 @@ class PublishEntity: last_modified_time: str name: str publish_domain_ref_id: str - entity_type: str - entity_ref_id: str + owner: str external_id: str status: PublishEntityStatus archival_reason: None | str | Unset = UNSET @@ -62,9 +60,7 @@ def to_dict(self) -> dict[str, Any]: publish_domain_ref_id = self.publish_domain_ref_id - entity_type = self.entity_type - - entity_ref_id = self.entity_ref_id + owner = self.owner external_id = self.external_id @@ -93,8 +89,7 @@ def to_dict(self) -> dict[str, Any]: "last_modified_time": last_modified_time, "name": name, "publish_domain_ref_id": publish_domain_ref_id, - "entity_type": entity_type, - "entity_ref_id": entity_ref_id, + "owner": owner, "external_id": external_id, "status": status, } @@ -123,9 +118,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: publish_domain_ref_id = d.pop("publish_domain_ref_id") - entity_type = d.pop("entity_type") - - entity_ref_id = d.pop("entity_ref_id") + owner = d.pop("owner") external_id = d.pop("external_id") @@ -157,8 +150,7 @@ def _parse_archived_time(data: object) -> None | str | Unset: last_modified_time=last_modified_time, name=name, publish_domain_ref_id=publish_domain_ref_id, - entity_type=entity_type, - entity_ref_id=entity_ref_id, + owner=owner, external_id=external_id, status=status, archival_reason=archival_reason, diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_args.py index f0bec80e4..af6ee2085 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_args.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_create_args.py @@ -14,30 +14,20 @@ class PublishEntityCreateArgs: """PublishEntityCreate args. Attributes: - name (str): The name of a publish entity. - entity_type (str): - entity_ref_id (str): A generic entity id. + owner (str): A reference combining an entity kind, a purpose, and an entity id. """ - name: str - entity_type: str - entity_ref_id: str + owner: str additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: - name = self.name - - entity_type = self.entity_type - - entity_ref_id = self.entity_ref_id + owner = self.owner field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( { - "name": name, - "entity_type": entity_type, - "entity_ref_id": entity_ref_id, + "owner": owner, } ) @@ -46,16 +36,10 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) - name = d.pop("name") - - entity_type = d.pop("entity_type") - - entity_ref_id = d.pop("entity_ref_id") + owner = d.pop("owner") publish_entity_create_args = cls( - name=name, - entity_type=entity_type, - entity_ref_id=entity_ref_id, + owner=owner, ) publish_entity_create_args.additional_properties = d diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/todo_task_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/todo_task_load_public_args.py new file mode 100644 index 000000000..aa1904057 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/todo_task_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="TodoTaskLoadPublicArgs") + + +@_attrs_define +class TodoTaskLoadPublicArgs: + """TodoTaskLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + todo_task_load_public_args = cls( + external_id=external_id, + ) + + todo_task_load_public_args.additional_properties = d + return todo_task_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/todo_task_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/todo_task_load_result.py index 26f91e3f9..d5d4fbe4c 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/todo_task_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/todo_task_load_result.py @@ -15,6 +15,7 @@ from ..models.goal import Goal from ..models.inbox_task import InboxTask from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_in_day_block import TimeEventInDayBlock from ..models.todo_task import TodoTask @@ -37,6 +38,7 @@ class TodoTaskLoadResult: chapter (Chapter | None | Unset): goal (Goal | None | Unset): note (None | Note | Unset): + publish_entity (None | PublishEntity | Unset): """ todo_task: TodoTask @@ -48,12 +50,14 @@ class TodoTaskLoadResult: chapter: Chapter | None | Unset = UNSET goal: Goal | None | Unset = UNSET note: None | Note | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.chapter import Chapter from ..models.goal import Goal from ..models.note import Note + from ..models.publish_entity import PublishEntity todo_task = self.todo_task.to_dict() @@ -100,6 +104,14 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -118,6 +130,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["goal"] = goal if note is not UNSET: field_dict["note"] = note + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -129,6 +143,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.goal import Goal from ..models.inbox_task import InboxTask from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_in_day_block import TimeEventInDayBlock from ..models.todo_task import TodoTask @@ -212,6 +227,23 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + todo_task_load_result = cls( todo_task=todo_task, inbox_task=inbox_task, @@ -222,6 +254,7 @@ def _parse_note(data: object) -> None | Note | Unset: chapter=chapter, goal=goal, note=note, + publish_entity=publish_entity, ) todo_task_load_result.additional_properties = d diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index 6539aefb4..ba34e51fe 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -757,6 +757,7 @@ export type { TodoTaskFindArgs } from './models/TodoTaskFindArgs'; export type { TodoTaskFindResult } from './models/TodoTaskFindResult'; export type { TodoTaskFindResultEntry } from './models/TodoTaskFindResultEntry'; export type { TodoTaskLoadArgs } from './models/TodoTaskLoadArgs'; +export type { TodoTaskLoadPublicArgs } from './models/TodoTaskLoadPublicArgs'; export type { TodoTaskLoadResult } from './models/TodoTaskLoadResult'; export type { TodoTaskName } from './models/TodoTaskName'; export type { TodoTaskRemoveArgs } from './models/TodoTaskRemoveArgs'; diff --git a/gen/ts/webapi-client/gen/models/PublishEntity.ts b/gen/ts/webapi-client/gen/models/PublishEntity.ts index 27b133c31..cc06cad3f 100644 --- a/gen/ts/webapi-client/gen/models/PublishEntity.ts +++ b/gen/ts/webapi-client/gen/models/PublishEntity.ts @@ -3,6 +3,7 @@ /* tslint:disable */ /* eslint-disable */ import type { EntityId } from './EntityId'; +import type { EntityLink } from './EntityLink'; import type { PublishEntityName } from './PublishEntityName'; import type { PublishEntityStatus } from './PublishEntityStatus'; import type { PublishExternalId } from './PublishExternalId'; @@ -20,8 +21,7 @@ export type PublishEntity = { archived_time?: (Timestamp | null); name: PublishEntityName; publish_domain_ref_id: string; - entity_type: string; - entity_ref_id: EntityId; + owner: EntityLink; external_id: PublishExternalId; status: PublishEntityStatus; }; diff --git a/gen/ts/webapi-client/gen/models/PublishEntityCreateArgs.ts b/gen/ts/webapi-client/gen/models/PublishEntityCreateArgs.ts index bf7acd8af..85c23166a 100644 --- a/gen/ts/webapi-client/gen/models/PublishEntityCreateArgs.ts +++ b/gen/ts/webapi-client/gen/models/PublishEntityCreateArgs.ts @@ -2,14 +2,11 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { EntityId } from './EntityId'; -import type { PublishEntityName } from './PublishEntityName'; +import type { EntityLink } from './EntityLink'; /** * PublishEntityCreate args. */ export type PublishEntityCreateArgs = { - name: PublishEntityName; - entity_type: string; - entity_ref_id: EntityId; + owner: EntityLink; }; diff --git a/gen/ts/webapi-client/gen/models/TodoTaskLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/TodoTaskLoadPublicArgs.ts new file mode 100644 index 000000000..6e31ec391 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/TodoTaskLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * TodoTaskLoadPublic args. + */ +export type TodoTaskLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/TodoTaskLoadResult.ts b/gen/ts/webapi-client/gen/models/TodoTaskLoadResult.ts index 9955725fe..eb9216007 100644 --- a/gen/ts/webapi-client/gen/models/TodoTaskLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/TodoTaskLoadResult.ts @@ -8,6 +8,7 @@ import type { Contact } from './Contact'; import type { Goal } from './Goal'; import type { InboxTask } from './InboxTask'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { Tag } from './Tag'; import type { TimeEventInDayBlock } from './TimeEventInDayBlock'; import type { TodoTask } from './TodoTask'; @@ -23,6 +24,7 @@ export type TodoTaskLoadResult = { tags: Array; contacts: Array; note?: (Note | null); + publish_entity?: (PublishEntity | null); time_event_blocks: Array; }; diff --git a/gen/ts/webapi-client/gen/services/TodoService.ts b/gen/ts/webapi-client/gen/services/TodoService.ts index de18a2165..18e0b79b2 100644 --- a/gen/ts/webapi-client/gen/services/TodoService.ts +++ b/gen/ts/webapi-client/gen/services/TodoService.ts @@ -8,6 +8,7 @@ import type { TodoTaskCreateResult } from '../models/TodoTaskCreateResult'; import type { TodoTaskFindArgs } from '../models/TodoTaskFindArgs'; import type { TodoTaskFindResult } from '../models/TodoTaskFindResult'; import type { TodoTaskLoadArgs } from '../models/TodoTaskLoadArgs'; +import type { TodoTaskLoadPublicArgs } from '../models/TodoTaskLoadPublicArgs'; import type { TodoTaskLoadResult } from '../models/TodoTaskLoadResult'; import type { TodoTaskRemoveArgs } from '../models/TodoTaskRemoveArgs'; import type { TodoTaskUpdateArgs } from '../models/TodoTaskUpdateArgs'; @@ -128,6 +129,34 @@ export class TodoService { }, }); } + /** + * Load a published todo task and its dependent entities by publish external id. + * @param requestBody The input data + * @returns TodoTaskLoadResult Successful response + * @throws ApiError + */ + public todoTaskLoadPublic( + requestBody?: TodoTaskLoadPublicArgs, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/todo-task-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * The command for removing a todo task. * @param requestBody The input data diff --git a/itests/api/common/publish.test.py b/itests/api/common/publish.test.py index 5d999790a..9b1fa0f51 100644 --- a/itests/api/common/publish.test.py +++ b/itests/api/common/publish.test.py @@ -4,15 +4,15 @@ import pytest import requests +from jupiter_webapi_client.api.prm.person_create import ( + sync_detailed as person_create_sync, +) from jupiter_webapi_client.api.publish.publish_entity_activate import ( sync_detailed as publish_entity_activate_sync, ) from jupiter_webapi_client.api.publish.publish_entity_create import ( sync_detailed as publish_entity_create_sync, ) -from jupiter_webapi_client.api.prm.person_create import ( - sync_detailed as person_create_sync, -) from jupiter_webapi_client.api.test_helper.workspace_set_feature import ( sync_detailed as workspace_set_feature_sync, ) @@ -37,7 +37,9 @@ from itests.helpers import get_parsed_from_response -_SHAREABLE_ENTITY_TYPE = "Person" + +def _person_owner(ref_id: str) -> str: + return f"Person:std:{ref_id}" @pytest.fixture(autouse=True, scope="module") @@ -69,16 +71,12 @@ def _create(name: str) -> Person: @pytest.fixture() def create_publish_entity(logged_in_client: AuthenticatedClient, create_person): - def _create( - name: str, entity_type: str = _SHAREABLE_ENTITY_TYPE - ) -> PublishEntity: + def _create(name: str, owner: str | None = None) -> PublishEntity: person = create_person(f"person-for-{name}") result = publish_entity_create_sync( client=logged_in_client, body=PublishEntityCreateArgs( - name=name, - entity_type=entity_type, - entity_ref_id=person.ref_id, + owner=owner or _person_owner(person.ref_id), ), ) return get_parsed_from_response( @@ -117,9 +115,7 @@ def test_api_common_publish_entity_create( _publish_entities_url(api_url), headers=_headers(api_key), json={ - "name": "draft-publish", - "entity_type": _SHAREABLE_ENTITY_TYPE, - "entity_ref_id": person.ref_id, + "owner": _person_owner(person.ref_id), }, timeout=10, ) @@ -127,7 +123,9 @@ def test_api_common_publish_entity_create( publish_entity = response.json()["new_publish_entity"] assert publish_entity["status"] == "draft" + assert publish_entity["name"] == "PublishEntity" assert publish_entity["external_id"] + assert publish_entity["owner"] == _person_owner(person.ref_id) def test_api_common_publish_entity_create_rejects_non_shareable_entity_type( @@ -139,9 +137,7 @@ def test_api_common_publish_entity_create_rejects_non_shareable_entity_type( _publish_entities_url(api_url), headers=_headers(api_key), json={ - "name": "invalid-publish", - "entity_type": "TodoTask", - "entity_ref_id": person.ref_id, + "owner": f"HomeTab:std:{person.ref_id}", }, timeout=10, ) @@ -193,9 +189,7 @@ def test_api_common_publish_entity_create_duplicate_entity_raises_already_exists _publish_entities_url(api_url), headers=_headers(api_key), json={ - "name": "another-name", - "entity_type": _SHAREABLE_ENTITY_TYPE, - "entity_ref_id": publish_entity.entity_ref_id, + "owner": publish_entity.owner, }, timeout=10, ) diff --git a/itests/api/todos.test.py b/itests/api/todos.test.py index e64dbabc8..405e00000 100644 --- a/itests/api/todos.test.py +++ b/itests/api/todos.test.py @@ -4,6 +4,12 @@ import pytest import requests +from jupiter_webapi_client.api.publish.publish_entity_activate import ( + sync_detailed as publish_entity_activate_sync, +) +from jupiter_webapi_client.api.publish.publish_entity_create import ( + sync_detailed as publish_entity_create_sync, +) from jupiter_webapi_client.api.test_helper.workspace_set_feature import ( sync_detailed as workspace_set_feature_sync, ) @@ -13,13 +19,30 @@ from jupiter_webapi_client.api.todo.todo_task_create import ( sync_detailed as todo_task_create_sync, ) -from jupiter_webapi_client.client import AuthenticatedClient +from jupiter_webapi_client.api.todo.todo_task_load_public import ( + sync_detailed as todo_task_load_public_sync, +) +from jupiter_webapi_client.client import AuthenticatedClient, Client from jupiter_webapi_client.models.difficulty import Difficulty from jupiter_webapi_client.models.eisen import Eisen +from jupiter_webapi_client.models.publish_entity import PublishEntity +from jupiter_webapi_client.models.publish_entity_activate_args import ( + PublishEntityActivateArgs, +) +from jupiter_webapi_client.models.publish_entity_create_args import ( + PublishEntityCreateArgs, +) +from jupiter_webapi_client.models.publish_entity_create_result import ( + PublishEntityCreateResult, +) from jupiter_webapi_client.models.todo_task import TodoTask from jupiter_webapi_client.models.todo_task_archive_args import TodoTaskArchiveArgs from jupiter_webapi_client.models.todo_task_create_args import TodoTaskCreateArgs from jupiter_webapi_client.models.todo_task_create_result import TodoTaskCreateResult +from jupiter_webapi_client.models.todo_task_load_public_args import ( + TodoTaskLoadPublicArgs, +) +from jupiter_webapi_client.models.todo_task_load_result import TodoTaskLoadResult from jupiter_webapi_client.models.workspace_feature import WorkspaceFeature from jupiter_webapi_client.models.workspace_set_feature_args import ( WorkspaceSetFeatureArgs, @@ -81,6 +104,35 @@ def _headers(api_key: str) -> dict[str, str]: return {"Authorization": f"Bearer {api_key}"} +def _todo_owner(ref_id: str) -> str: + return f"TodoTask:std:{ref_id}" + + +@pytest.fixture() +def create_and_activate_todo_publish( + logged_in_client: AuthenticatedClient, create_todo +): + def _create(name: str) -> tuple[TodoTask, PublishEntity]: + todo_task = create_todo(name) + create_result = publish_entity_create_sync( + client=logged_in_client, + body=PublishEntityCreateArgs( + owner=_todo_owner(todo_task.ref_id), + ), + ) + publish_entity = get_parsed_from_response( + PublishEntityCreateResult, create_result + ).new_publish_entity + activate_result = publish_entity_activate_sync( + client=logged_in_client, + body=PublishEntityActivateArgs(ref_id=publish_entity.ref_id), + ) + assert activate_result.status_code == 200 + return todo_task, publish_entity + + return _create + + def test_api_todo_create(api_url: str, api_key: str) -> None: response = requests.post( f"{api_url}/v1/todos", @@ -109,6 +161,34 @@ def test_api_todo_create(api_url: str, api_key: str) -> None: assert inbox_task["difficulty"] == "medium" +def test_webapi_todo_load_public( + webapi_url: str, create_and_activate_todo_publish +) -> None: + guest_client = Client(base_url=webapi_url) + todo_task, publish_entity = create_and_activate_todo_publish("Public Todo") + + result = todo_task_load_public_sync( + client=guest_client, + body=TodoTaskLoadPublicArgs(external_id=publish_entity.external_id), + ) + assert result.status_code == 200 + + loaded = get_parsed_from_response(TodoTaskLoadResult, result) + assert loaded.todo_task.ref_id == todo_task.ref_id + assert loaded.todo_task.name == "Public Todo" + assert loaded.inbox_task.name == "Public Todo" + + +def test_webapi_todo_load_public_not_found(webapi_url: str) -> None: + guest_client = Client(base_url=webapi_url) + + result = todo_task_load_public_sync( + client=guest_client, + body=TodoTaskLoadPublicArgs(external_id="00000000-0000-4000-8000-000000000000"), + ) + assert result.status_code == 404 + + def test_api_todo_load(api_url: str, api_key: str, create_todo) -> None: created = create_todo("Load Todo") diff --git a/src/api/jupiter/api/jupiter.py b/src/api/jupiter/api/jupiter.py index 70e7c3907..67bc07716 100644 --- a/src/api/jupiter/api/jupiter.py +++ b/src/api/jupiter/api/jupiter.py @@ -440,6 +440,20 @@ from jupiter_webapi_client.api.prm.person_update import ( asyncio_detailed as person_update, ) + +# --- Publish API --- +from jupiter_webapi_client.api.publish.publish_entity_activate import ( + asyncio_detailed as publish_entity_activate, +) +from jupiter_webapi_client.api.publish.publish_entity_create import ( + asyncio_detailed as publish_entity_create, +) +from jupiter_webapi_client.api.publish.publish_entity_load_by_external_id import ( + asyncio_detailed as publish_entity_load_by_external_id, +) +from jupiter_webapi_client.api.publish.publish_entity_to_draft import ( + asyncio_detailed as publish_entity_to_draft, +) from jupiter_webapi_client.api.schedule.schedule_event_full_days_archive import ( asyncio_detailed as schedule_event_full_days_archive, ) @@ -558,20 +572,6 @@ asyncio_detailed as smart_list_update, ) -# --- Publish API --- -from jupiter_webapi_client.api.publish.publish_entity_activate import ( - asyncio_detailed as publish_entity_activate, -) -from jupiter_webapi_client.api.publish.publish_entity_create import ( - asyncio_detailed as publish_entity_create, -) -from jupiter_webapi_client.api.publish.publish_entity_load_by_external_id import ( - asyncio_detailed as publish_entity_load_by_external_id, -) -from jupiter_webapi_client.api.publish.publish_entity_to_draft import ( - asyncio_detailed as publish_entity_to_draft, -) - # --- Tags API --- from jupiter_webapi_client.api.tags.tag_archive import ( asyncio_detailed as tag_archive, diff --git a/src/cli/jupiter/cli/command/exceptions.py b/src/cli/jupiter/cli/command/exceptions.py index b1bdcf0fd..b927702e9 100644 --- a/src/cli/jupiter/cli/command/exceptions.py +++ b/src/cli/jupiter/cli/command/exceptions.py @@ -159,9 +159,7 @@ def handle(self, console: Console, exception: ContactAlreadyExistsError) -> None sys.exit(1) -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" def handle(self, console: Console, exception: EntityIsAlreadyActiveError) -> None: diff --git a/src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx b/src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx new file mode 100644 index 000000000..780a43d85 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx @@ -0,0 +1,130 @@ +import type { + EntityId, + NamedEntityTag, + PublishEntity, +} from "@jupiter/webapi-client"; +import { PublishEntityStatus } from "@jupiter/webapi-client"; +import { + Button, + FormControl, + InputAdornment, + InputLabel, + OutlinedInput, + Stack, + Typography, +} from "@mui/material"; +import { useContext } from "react"; + +import { entityLinkStd } from "#/core/common/entity-link"; +import { ServicePropertiesContext } from "#/core/config-client"; +import { + ActionSingle, + SectionActions, +} from "#/core/infra/component/section-actions"; +import { SectionCard } from "#/core/infra/component/section-card"; +import type { TopLevelInfo } from "#/core/infra/top-level-context"; + +interface PublishPanelProps { + entityType: NamedEntityTag; + entityRefId: EntityId; + topLevelInfo: TopLevelInfo; + inputsEnabled: boolean; + publishEntity: PublishEntity | null; +} + +export function PublishPanel(props: PublishPanelProps) { + const serviceProperties = useContext(ServicePropertiesContext); + const sectionId = `${props.entityType}-publish`; + const publishOwner = entityLinkStd(props.entityType, props.entityRefId); + const publicUrl = + props.publishEntity !== null + ? `${serviceProperties.webUiUrl}/app/public/published/${props.publishEntity.external_id}` + : ""; + const isActive = props.publishEntity?.status === PublishEntityStatus.ACTIVE; + + return ( + + ) : ( + + ) + } + > + {props.publishEntity === null ? ( + + Publish this entity to share a read-only link with others. + + ) : ( + + + Status:{" "} + + {props.publishEntity.status} + + + + + Public URL + + + + } + /> + + + )} + + + {props.publishEntity !== null && ( + + )} + + ); +} diff --git a/src/core/jupiter/core/common/sub/publish/impl/postgres_repository.py b/src/core/jupiter/core/common/sub/publish/impl/postgres_repository.py index ac7e4b3e9..2bdf5705a 100644 --- a/src/core/jupiter/core/common/sub/publish/impl/postgres_repository.py +++ b/src/core/jupiter/core/common/sub/publish/impl/postgres_repository.py @@ -6,6 +6,7 @@ PublishEntityAlreadyExistsError, PublishEntityRepository, ) +from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.realm.realm import RealmCodecRegistry from jupiter.framework.storage.postgres.repository import PostgresLeafEntityRepository from jupiter.framework.storage.repository import EntityNotFoundError @@ -33,6 +34,22 @@ def __init__( already_exists_err_cls=PublishEntityAlreadyExistsError, ) + async def load_optional_for_owner( + self, + owner: EntityLink, + allow_archived: bool = False, + ) -> PublishEntity | None: + """Load a publish entity by its owner link.""" + encoded = self._realm_codec_registry.db_encode(owner) + query_stmt = select(self._table).where(self._table.c.owner == encoded) + if not allow_archived: + query_stmt = query_stmt.where(self._table.c.archived.is_(False)) + + result = (await self._connection.execute(query_stmt)).first() + if result is None: + return None + return self._row_to_entity(result) + async def load_by_external_id( self, external_id: PublishExternalId, diff --git a/src/core/jupiter/core/common/sub/publish/impl/sqlite_repository.py b/src/core/jupiter/core/common/sub/publish/impl/sqlite_repository.py index d714985d9..c1c515c94 100644 --- a/src/core/jupiter/core/common/sub/publish/impl/sqlite_repository.py +++ b/src/core/jupiter/core/common/sub/publish/impl/sqlite_repository.py @@ -6,6 +6,7 @@ PublishEntityAlreadyExistsError, PublishEntityRepository, ) +from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.realm.realm import RealmCodecRegistry from jupiter.framework.storage.repository import EntityNotFoundError from jupiter.framework.storage.sqlite.repository import SqliteLeafEntityRepository @@ -33,6 +34,22 @@ def __init__( already_exists_err_cls=PublishEntityAlreadyExistsError, ) + async def load_optional_for_owner( + self, + owner: EntityLink, + allow_archived: bool = False, + ) -> PublishEntity | None: + """Load a publish entity by its owner link.""" + encoded = self._realm_codec_registry.db_encode(owner) + query_stmt = select(self._table).where(self._table.c.owner == encoded) + if not allow_archived: + query_stmt = query_stmt.where(self._table.c.archived.is_(False)) + + result = (await self._connection.execute(query_stmt)).first() + if result is None: + return None + return self._row_to_entity(result) + async def load_by_external_id( self, external_id: PublishExternalId, diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index ac3e67a7e..4470f5e19 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -8,6 +8,7 @@ from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.context import DomainContext from jupiter.framework.entity import ( LeafEntity, @@ -22,9 +23,10 @@ LeafEntityRepository, ) -# Allowed ``NamedEntityTag`` values for shareable :class:`PublishEntity` targets. -ALLOWED_PUBLISH_ENTITY_TYPES: Final[frozenset[str]] = frozenset( +# Allowed ``EntityLink.the_type`` values for shareable :class:`PublishEntity` owners. +ALLOWED_PUBLISH_OWNER_TYPES: Final[frozenset[str]] = frozenset( { + NamedEntityTag.TODO_TASK.value, NamedEntityTag.WORKING_MEM.value, NamedEntityTag.TIME_PLAN.value, NamedEntityTag.SCHEDULE_STREAM.value, @@ -63,14 +65,16 @@ class EntityIsAlreadyDraftError(Exception): """Error raised when trying to move an already draft entity to draft.""" +DEFAULT_PUBLISH_ENTITY_NAME = PublishEntityName("PublishEntity") + + @entity("PublishDomain") class PublishEntity(LeafEntity): """A publish entity.""" publish_domain: ParentLink name: PublishEntityName - entity_type: str - entity_ref_id: EntityId + owner: EntityLink external_id: PublishExternalId status: PublishEntityStatus @@ -79,21 +83,22 @@ class PublishEntity(LeafEntity): def new_publish_entity( ctx: DomainContext, publish_domain_ref_id: EntityId, - name: PublishEntityName, - entity_type: str, - entity_ref_id: EntityId, + owner: EntityLink, ) -> "PublishEntity": """Create a publish entity.""" - if entity_type not in ALLOWED_PUBLISH_ENTITY_TYPES: + if owner.the_type not in ALLOWED_PUBLISH_OWNER_TYPES: + raise InputValidationError( + f"Invalid publish entity owner type: {owner.the_type!r}", + ) + if owner.purpose != "std": raise InputValidationError( - f"Invalid publish entity type: {entity_type!r}", + f"Publish entity owner link purpose must be 'std', got {owner.purpose!r}", ) return PublishEntity._create( ctx, publish_domain=ParentLink(publish_domain_ref_id), - name=name, - entity_type=entity_type, - entity_ref_id=entity_ref_id, + name=DEFAULT_PUBLISH_ENTITY_NAME, + owner=owner, external_id=PublishExternalId.new_external_id(), status=PublishEntityStatus.DRAFT, ) @@ -120,6 +125,14 @@ def to_draft(self, ctx: DomainContext) -> "PublishEntity": class PublishEntityRepository(LeafEntityRepository[PublishEntity], abc.ABC): """A repository for publish entities.""" + @abc.abstractmethod + async def load_optional_for_owner( + self, + owner: EntityLink, + allow_archived: bool = False, + ) -> PublishEntity | None: + """Load a publish entity by its owner link.""" + @abc.abstractmethod async def load_by_external_id( self, diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.test.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.test.py index 84d2c590c..525ce1d25 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.test.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.test.py @@ -1,14 +1,13 @@ """Tests for publish entity.""" -from jupiter.core.common.sub.publish.sub.entity.root import ( - ALLOWED_PUBLISH_ENTITY_TYPES, -) +from jupiter.core.common.sub.publish.sub.entity.root import ALLOWED_PUBLISH_OWNER_TYPES from jupiter.core.named_entity_tag import NamedEntityTag -def test_allowed_publish_entity_types_matches_shareable_named_entity_tags() -> None: - assert ALLOWED_PUBLISH_ENTITY_TYPES == frozenset( +def test_allowed_publish_owner_types_matches_shareable_named_entity_tags() -> None: + assert ALLOWED_PUBLISH_OWNER_TYPES == frozenset( { + NamedEntityTag.TODO_TASK.value, NamedEntityTag.WORKING_MEM.value, NamedEntityTag.TIME_PLAN.value, NamedEntityTag.SCHEDULE_STREAM.value, @@ -33,7 +32,3 @@ def test_allowed_publish_entity_types_matches_shareable_named_entity_tags() -> N NamedEntityTag.PERSON.value, } ) - - -def test_todo_task_is_not_an_allowed_publish_entity_type() -> None: - assert NamedEntityTag.TODO_TASK.value not in ALLOWED_PUBLISH_ENTITY_TYPES diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/create.py b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/create.py index e3c50450f..7690fb78d 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/create.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/create.py @@ -1,13 +1,12 @@ """Use case for creating a publish entity.""" from jupiter.core.common.sub.publish.root import PublishDomain -from jupiter.core.common.sub.publish.sub.entity.name import PublishEntityName from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.config import ( JupiterLoggedInMutationContext, JupiterTransactionalLoggedInMutationUseCase, ) -from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.progress_reporter.reporter import ProgressReporter from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import mutation_use_case @@ -23,9 +22,7 @@ class PublishEntityCreateArgs(UseCaseArgsBase): """PublishEntityCreate args.""" - name: PublishEntityName - entity_type: str - entity_ref_id: EntityId + owner: EntityLink @use_case_result @@ -59,9 +56,7 @@ async def _perform_transactional_mutation( new_publish_entity = PublishEntity.new_publish_entity( ctx=context.domain_context, publish_domain_ref_id=publish_domain.ref_id, - name=args.name, - entity_type=args.entity_type, - entity_ref_id=args.entity_ref_id, + owner=args.owner, ) new_publish_entity = await uow.get_for(PublishEntity).create(new_publish_entity) return PublishEntityCreateResult(new_publish_entity=new_publish_entity) diff --git a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx index dded75a30..766ae75ab 100644 --- a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx +++ b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx @@ -7,6 +7,7 @@ import { History as HistoryIcon, KeyboardDoubleArrowRight as KeyboardDoubleArrowRightIcon, PictureInPictureAlt as PictureInPictureAltIcon, + Public as PublicIcon, SwitchLeft as SwitchLeftIcon, } from "@mui/icons-material"; import { @@ -24,8 +25,9 @@ import { import { Form, useNavigate } from "@remix-run/react"; import { motion, useIsPresent } from "framer-motion"; import type { PropsWithChildren } from "react"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { EntityId, NamedEntityTag } from "@jupiter/webapi-client"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; +import { PublishPanel } from "#/core/common/sub/publish/components/publish-panel"; +import { EntityId, NamedEntityTag, type PublishEntity } from "@jupiter/webapi-client"; import { LeafPanelExpansionState, @@ -39,6 +41,7 @@ import { } from "#/core/infra/scroll-restoration"; import { useBigScreen } from "#/core/infra/component/use-big-screen"; import { EntityMutationHistoryPanel } from "#/core/infra/component/layout/entity-mutation-history-panel"; +import { TopLevelInfoContext } from "#/core/infra/top-level-context"; const BIG_SCREEN_ANIMATION_START = "480px"; const BIG_SCREEN_ANIMATION_END = "480px"; @@ -68,10 +71,14 @@ interface LeafPanelProps { initialExpansionState?: LeafPanelExpansionState; allowedExpansionStates?: LeafPanelExpansionState[]; shouldShowALeaflet?: boolean; + publishable?: boolean; + publishEntity?: PublishEntity; + disabled?: boolean; } export function LeafPanel(props: PropsWithChildren) { const isBigScreen = useBigScreen(); + const topLevelInfo = useContext(TopLevelInfoContext); const navigation = useNavigate(); const containerRef = useRef(null); const isPresent = useIsPresent(); @@ -94,9 +101,14 @@ export function LeafPanel(props: PropsWithChildren) { ); const [showArchiveDialog, setShowArchiveDialog] = useState(false); const [showHistory, setShowHistory] = useState(false); + const [showPublish, setShowPublish] = useState(false); const hasHistory = props.entityType !== undefined && props.entityRefId !== undefined; + const hasPublish = + props.publishable === true && + props.entityType !== undefined && + props.entityRefId !== undefined; const showArchiveButNotRemove = props.showArchiveButton && !props.showArchiveAndRemoveButton; @@ -303,6 +315,9 @@ export function LeafPanel(props: PropsWithChildren) { }, }; + const showControls = + !props.disabled && (isBigScreen || !props.shouldShowALeaflet); + return ( ) { transition={{ duration: 0.5 }} isBigScreen={isBigScreen} > - {(isBigScreen || !props.shouldShowALeaflet) && ( + {showControls && (
@@ -364,20 +379,38 @@ export function LeafPanel(props: PropsWithChildren) { - {hasHistory && ( - setShowHistory((h) => !h)} - > - - + {(hasHistory || hasPublish) && ( + + {hasHistory && ( + { + setShowPublish(false); + setShowHistory((h) => !h); + }} + > + + + )} + {hasPublish && ( + { + setShowHistory(false); + setShowPublish((p) => !p); + }} + > + + + )} + )} {(props.showArchiveButton || props.showArchiveAndRemoveButton) && ( <> ) { id="leaf-panel-content" ref={containerRef} isbigscreen={isBigScreen ? "true" : "false"} + showcontrols={showControls ? "true" : "false"} > + ) : showPublish && hasPublish ? ( + + + + + + ) : ( <> {(isBigScreen || !props.shouldShowALeaflet) && ( @@ -452,6 +504,7 @@ export function LeafPanel(props: PropsWithChildren) { id="leaf-panel-content" ref={containerRef} isbigscreen={isBigScreen ? "true" : "false"} + showcontrols={showControls ? "true" : "false"} > {props.children} @@ -499,13 +552,18 @@ const LeafPanelControls = styled("div")( interface LeafPanelContentProps { isbigscreen: string; + showcontrols: string; } const LeafPanelContent = styled("div")( - ({ isbigscreen }) => ({ - padding: "0.5rem", + ({ isbigscreen, showcontrols }) => ({ + padding: showcontrols === "true" ? "0.5rem" : "1.5rem 0.5rem 0.5rem", height: `calc(var(--vh, 1vh) * 100 - env(safe-area-inset-top) - 4rem - ${ - isbigscreen === "true" ? "4rem" : "3.5rem" + showcontrols === "true" + ? isbigscreen === "true" + ? "4rem" + : "3.5rem" + : "0rem" })`, overflowY: "scroll", }), diff --git a/src/core/jupiter/core/todo/root.py b/src/core/jupiter/core/todo/root.py index 6b747b038..8ec247fa7 100644 --- a/src/core/jupiter/core/todo/root.py +++ b/src/core/jupiter/core/todo/root.py @@ -3,6 +3,7 @@ from jupiter.core.common.sub.contacts.sub.link.root import ContactLink from jupiter.core.common.sub.inbox_tasks.root import InboxTask from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( TimeEventInDayBlock, @@ -52,6 +53,9 @@ class TodoTask(LeafEntity): owner=IsEntityLinkStd(NamedEntityTag.TODO_TASK.value), ) note = OwnsAtMostOne(Note, owner=IsEntityLinkStd(NamedEntityTag.TODO_TASK.value)) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.TODO_TASK.value) + ) @staticmethod @create_entity_action diff --git a/src/core/jupiter/core/todo/service/load.py b/src/core/jupiter/core/todo/service/load.py new file mode 100644 index 000000000..d5d274ab2 --- /dev/null +++ b/src/core/jupiter/core/todo/service/load.py @@ -0,0 +1,147 @@ +"""Shared service for loading a todo task and its dependent entities.""" + +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.inbox_tasks.collection import InboxTaskCollection +from jupiter.core.common.sub.inbox_tasks.root import InboxTask, InboxTaskRepository +from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.common.sub.time_events.domain import TimeEventDomain +from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( + TimeEventInDayBlock, +) +from jupiter.core.life_plan.sub.aspects.root import Aspect +from jupiter.core.life_plan.sub.chapters.root import Chapter +from jupiter.core.life_plan.sub.goals.root import Goal +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.todo.root import TodoTask +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.errors import InputValidationError +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result + + +@use_case_result +class TodoTaskLoadResult(UseCaseResultBase): + """TodoTaskLoadResult.""" + + todo_task: TodoTask + inbox_task: InboxTask + aspect: Aspect + chapter: Chapter | None + goal: Goal | None + tags: list[Tag] + contacts: list[Contact] + note: Note | None + publish_entity: PublishEntity | None + time_event_blocks: list[TimeEventInDayBlock] + + +class TodoTaskLoadService: + """Shared service for loading a todo task and its dependent entities.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + todo_task: TodoTask, + *, + allow_archived: bool = False, + ) -> TodoTaskLoadResult: + """Load a todo task together with the entities that hang off it.""" + aspect = await uow.get_for(Aspect).load_by_id(todo_task.aspect_ref_id) + chapter = ( + await uow.get_for(Chapter).load_by_id(todo_task.chapter_ref_id) + if todo_task.chapter_ref_id + else None + ) + goal = ( + await uow.get_for(Goal).load_by_id(todo_task.goal_ref_id) + if todo_task.goal_ref_id + else None + ) + + inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( + workspace_ref_id + ) + linked_inbox_tasks = await uow.get( + InboxTaskRepository + ).find_all_for_owner_created_desc( + parent_ref_id=inbox_task_collection.ref_id, + owner=EntityLink.std(NamedEntityTag.TODO_TASK.value, todo_task.ref_id), + allow_archived=allow_archived, + ) + if len(linked_inbox_tasks) == 0: + raise InputValidationError( + f"No inbox task associated with todo task '{todo_task.ref_id}'" + ) + if len(linked_inbox_tasks) > 1: + raise InputValidationError( + f"Multiple inbox tasks associated with todo task '{todo_task.ref_id}'" + ) + inbox_task = linked_inbox_tasks[0] + + note = await uow.get(NoteRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.TODO_TASK.value, todo_task.ref_id), + allow_archived=allow_archived, + ) + publish_entity = await uow.get(PublishEntityRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.TODO_TASK.value, todo_task.ref_id), + allow_archived=allow_archived, + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std(NamedEntityTag.TODO_TASK.value, todo_task.ref_id), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace_ref_id + ) + contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.TODO_TASK.value, todo_task.ref_id), + ) + if contact_link is not None: + contacts = await uow.get_for(Contact).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + ref_id=contact_link.contacts_ref_ids, + ) + else: + contacts = [] + + time_event_domain = await uow.get_for(TimeEventDomain).load_by_parent( + workspace_ref_id + ) + time_event_blocks = await uow.get_for(TimeEventInDayBlock).find_all_generic( + parent_ref_id=time_event_domain.ref_id, + allow_archived=False, + owner=EntityLink.std(NamedEntityTag.TODO_TASK.value, todo_task.ref_id), + ) + + return TodoTaskLoadResult( + todo_task=todo_task, + inbox_task=inbox_task, + aspect=aspect, + chapter=chapter, + goal=goal, + tags=tags, + contacts=contacts, + note=note, + publish_entity=publish_entity, + time_event_blocks=time_event_blocks, + ) diff --git a/src/core/jupiter/core/todo/use_case/load.py b/src/core/jupiter/core/todo/use_case/load.py index 0578b17c2..e6d344288 100644 --- a/src/core/jupiter/core/todo/use_case/load.py +++ b/src/core/jupiter/core/todo/use_case/load.py @@ -1,38 +1,18 @@ """Use case for loading a particular todo task.""" -from jupiter.core.common.sub.contacts.root import ContactDomain -from jupiter.core.common.sub.contacts.sub.contact.root import Contact -from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository -from jupiter.core.common.sub.inbox_tasks.collection import InboxTaskCollection -from jupiter.core.common.sub.inbox_tasks.root import InboxTask, InboxTaskRepository -from jupiter.core.common.sub.notes.root import Note, NoteRepository -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository -from jupiter.core.common.sub.time_events.domain import TimeEventDomain -from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( - TimeEventInDayBlock, -) from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.life_plan.sub.aspects.root import Aspect -from jupiter.core.life_plan.sub.chapters.root import Chapter -from jupiter.core.life_plan.sub.goals.root import Goal -from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.core.todo.root import TodoTask +from jupiter.core.todo.service.load import TodoTaskLoadResult, TodoTaskLoadService from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink -from jupiter.framework.errors import InputValidationError from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import readonly_use_case -from jupiter.framework.use_case_io import ( - UseCaseArgsBase, - UseCaseResultBase, - use_case_args, - use_case_result, -) +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + +__all__ = ["TodoTaskLoadArgs", "TodoTaskLoadResult", "TodoTaskLoadUseCase"] @use_case_args @@ -43,21 +23,6 @@ class TodoTaskLoadArgs(UseCaseArgsBase): allow_archived: bool | None -@use_case_result -class TodoTaskLoadResult(UseCaseResultBase): - """TodoTaskLoadResult.""" - - todo_task: TodoTask - inbox_task: InboxTask - aspect: Aspect - chapter: Chapter | None - goal: Goal | None - tags: list[Tag] - contacts: list[Contact] - note: Note | None - time_event_blocks: list[TimeEventInDayBlock] - - @readonly_use_case(WorkspaceFeature.TODO_TASK) class TodoTaskLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[TodoTaskLoadArgs, TodoTaskLoadResult] @@ -77,87 +42,10 @@ async def _perform_transactional_read( todo_task = await uow.get_for(TodoTask).load_by_id( args.ref_id, allow_archived=allow_archived ) - aspect = await uow.get_for(Aspect).load_by_id(todo_task.aspect_ref_id) - chapter = ( - await uow.get_for(Chapter).load_by_id(todo_task.chapter_ref_id) - if todo_task.chapter_ref_id - else None - ) - goal = ( - await uow.get_for(Goal).load_by_id(todo_task.goal_ref_id) - if todo_task.goal_ref_id - else None - ) - - inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( - workspace.ref_id - ) - linked_inbox_tasks = await uow.get( - InboxTaskRepository - ).find_all_for_owner_created_desc( - parent_ref_id=inbox_task_collection.ref_id, - owner=EntityLink.std(NamedEntityTag.TODO_TASK.value, todo_task.ref_id), - allow_archived=allow_archived, - ) - if len(linked_inbox_tasks) == 0: - raise InputValidationError( - f"No inbox task associated with todo task '{todo_task.ref_id}'" - ) - if len(linked_inbox_tasks) > 1: - raise InputValidationError( - f"Multiple inbox tasks associated with todo task '{todo_task.ref_id}'" - ) - inbox_task = linked_inbox_tasks[0] - note = await uow.get(NoteRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.TODO_TASK.value, todo_task.ref_id), + return await TodoTaskLoadService().do_it( + uow, + workspace.ref_id, + todo_task, allow_archived=allow_archived, ) - - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.TODO_TASK.value, todo_task.ref_id), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - - contact_domain = await uow.get_for(ContactDomain).load_by_parent( - workspace.ref_id - ) - contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.TODO_TASK.value, todo_task.ref_id), - ) - if contact_link is not None: - contacts = await uow.get_for(Contact).find_all_generic( - parent_ref_id=contact_domain.ref_id, - allow_archived=False, - ref_id=contact_link.contacts_ref_ids, - ) - else: - contacts = [] - - time_event_domain = await uow.get_for(TimeEventDomain).load_by_parent( - workspace.ref_id - ) - time_event_blocks = await uow.get_for(TimeEventInDayBlock).find_all_generic( - parent_ref_id=time_event_domain.ref_id, - allow_archived=False, - owner=EntityLink.std(NamedEntityTag.TODO_TASK.value, todo_task.ref_id), - ) - - return TodoTaskLoadResult( - todo_task=todo_task, - inbox_task=inbox_task, - aspect=aspect, - chapter=chapter, - goal=goal, - tags=tags, - contacts=contacts, - note=note, - time_event_blocks=time_event_blocks, - ) diff --git a/src/core/jupiter/core/todo/use_case/load_public.py b/src/core/jupiter/core/todo/use_case/load_public.py new file mode 100644 index 000000000..cab8a7cc3 --- /dev/null +++ b/src/core/jupiter/core/todo/use_case/load_public.py @@ -0,0 +1,71 @@ +"""Guest readonly use case for loading a published todo task by external id.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.todo.root import TodoTask +from jupiter.core.todo.service.load import TodoTaskLoadResult, TodoTaskLoadService +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import ( + UseCaseArgsBase, + use_case_args, +) + + +@use_case_args +class TodoTaskLoadPublicArgs(UseCaseArgsBase): + """TodoTaskLoadPublic args.""" + + external_id: PublishExternalId + + +class TodoTaskLoadPublicUseCase( + JupiterGuestReadonlyUseCase[TodoTaskLoadPublicArgs, TodoTaskLoadResult] +): + """Load a published todo task and its dependent entities by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: TodoTaskLoadPublicArgs, + ) -> TodoTaskLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.TODO_TASK.value: + raise InputValidationError( + "The publish entity does not refer to a todo task." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + todo_task = await uow.get_for(TodoTask).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + + return await TodoTaskLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + todo_task, + allow_archived=False, + ) diff --git a/src/core/migrations/postgres/versions/2026_06_07_00_29_publish_domain.py b/src/core/migrations/postgres/versions/2026_06_07_00_29_publish_domain.py index d97bf8ab6..a1c293c09 100644 --- a/src/core/migrations/postgres/versions/2026_06_07_00_29_publish_domain.py +++ b/src/core/migrations/postgres/versions/2026_06_07_00_29_publish_domain.py @@ -78,8 +78,7 @@ def upgrade() -> None: archived_time TIMESTAMP WITH TIME ZONE, publish_domain_ref_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, - entity_type VARCHAR(255) NOT NULL, - entity_ref_id INTEGER NOT NULL, + owner VARCHAR(256) NOT NULL, external_id VARCHAR(64) NOT NULL, status VARCHAR(32) NOT NULL, PRIMARY KEY (ref_id), @@ -104,14 +103,14 @@ def upgrade() -> None: op.execute( """ - CREATE UNIQUE INDEX ix_publish_entity_entity_type_entity_ref_id - ON publish_entity (entity_type, entity_ref_id) + CREATE UNIQUE INDEX ix_publish_entity_owner + ON publish_entity (owner) """ ) def downgrade() -> None: - op.execute("DROP INDEX ix_publish_entity_entity_type_entity_ref_id") + op.execute("DROP INDEX ix_publish_entity_owner") op.execute("DROP INDEX ix_publish_entity_external_id") op.execute("DROP INDEX ix_publish_entity_publish_domain_ref_id") op.execute("DROP TABLE publish_entity") diff --git a/src/core/migrations/sqlite/versions/2026_06_07_00_29_publish_domain.py b/src/core/migrations/sqlite/versions/2026_06_07_00_29_publish_domain.py index 213102d3f..9d9da7eeb 100644 --- a/src/core/migrations/sqlite/versions/2026_06_07_00_29_publish_domain.py +++ b/src/core/migrations/sqlite/versions/2026_06_07_00_29_publish_domain.py @@ -78,8 +78,7 @@ def upgrade() -> None: archived_time DATETIME, publish_domain_ref_id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, - entity_type VARCHAR(255) NOT NULL, - entity_ref_id INTEGER NOT NULL, + owner VARCHAR(256) NOT NULL, external_id VARCHAR(64) NOT NULL, status VARCHAR(32) NOT NULL, PRIMARY KEY (ref_id), @@ -104,14 +103,14 @@ def upgrade() -> None: op.execute( """ - CREATE UNIQUE INDEX ix_publish_entity_entity_type_entity_ref_id - ON publish_entity (entity_type, entity_ref_id) + CREATE UNIQUE INDEX ix_publish_entity_owner + ON publish_entity (owner) """ ) def downgrade() -> None: - op.execute("DROP INDEX ix_publish_entity_entity_type_entity_ref_id") + op.execute("DROP INDEX ix_publish_entity_owner") op.execute("DROP INDEX ix_publish_entity_external_id") op.execute("DROP INDEX ix_publish_entity_publish_domain_ref_id") op.execute("DROP TABLE publish_entity") diff --git a/src/mcp/jupiter/mcp/jupiter.py b/src/mcp/jupiter/mcp/jupiter.py index 996467fba..775b92aec 100644 --- a/src/mcp/jupiter/mcp/jupiter.py +++ b/src/mcp/jupiter/mcp/jupiter.py @@ -430,6 +430,20 @@ from jupiter_webapi_client.api.prm.person_update import ( asyncio_detailed as person_update, ) + +# --- Publish API --- +from jupiter_webapi_client.api.publish.publish_entity_activate import ( + asyncio_detailed as publish_entity_activate, +) +from jupiter_webapi_client.api.publish.publish_entity_create import ( + asyncio_detailed as publish_entity_create, +) +from jupiter_webapi_client.api.publish.publish_entity_load_by_external_id import ( + asyncio_detailed as publish_entity_load_by_external_id, +) +from jupiter_webapi_client.api.publish.publish_entity_to_draft import ( + asyncio_detailed as publish_entity_to_draft, +) from jupiter_webapi_client.api.schedule.schedule_event_full_days_archive import ( asyncio_detailed as schedule_event_full_days_archive, ) @@ -543,20 +557,6 @@ asyncio_detailed as smart_list_update, ) -# --- Publish API --- -from jupiter_webapi_client.api.publish.publish_entity_activate import ( - asyncio_detailed as publish_entity_activate, -) -from jupiter_webapi_client.api.publish.publish_entity_create import ( - asyncio_detailed as publish_entity_create, -) -from jupiter_webapi_client.api.publish.publish_entity_load_by_external_id import ( - asyncio_detailed as publish_entity_load_by_external_id, -) -from jupiter_webapi_client.api.publish.publish_entity_to_draft import ( - asyncio_detailed as publish_entity_to_draft, -) - # --- Tags API --- from jupiter_webapi_client.api.tags.tag_archive import ( asyncio_detailed as tag_archive, diff --git a/src/webapi/clear-abandoned-users-do-all/jupiter_webapi_clear_abandoned_users_do_all/exceptions.py b/src/webapi/clear-abandoned-users-do-all/jupiter_webapi_clear_abandoned_users_do_all/exceptions.py index 60f594c6e..74d2e3bb1 100644 --- a/src/webapi/clear-abandoned-users-do-all/jupiter_webapi_clear_abandoned_users_do_all/exceptions.py +++ b/src/webapi/clear-abandoned-users-do-all/jupiter_webapi_clear_abandoned_users_do_all/exceptions.py @@ -97,9 +97,7 @@ class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" diff --git a/src/webapi/crm-backfill-do-all/jupiter_webapi_crm_backfill_do_all/exceptions.py b/src/webapi/crm-backfill-do-all/jupiter_webapi_crm_backfill_do_all/exceptions.py index caaa3751e..66478adbc 100644 --- a/src/webapi/crm-backfill-do-all/jupiter_webapi_crm_backfill_do_all/exceptions.py +++ b/src/webapi/crm-backfill-do-all/jupiter_webapi_crm_backfill_do_all/exceptions.py @@ -97,9 +97,7 @@ class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" diff --git a/src/webapi/gc-do-all/jupiter_webapi_gc_do_all/exceptions.py b/src/webapi/gc-do-all/jupiter_webapi_gc_do_all/exceptions.py index 2a3c6e239..b66e67ef8 100644 --- a/src/webapi/gc-do-all/jupiter_webapi_gc_do_all/exceptions.py +++ b/src/webapi/gc-do-all/jupiter_webapi_gc_do_all/exceptions.py @@ -97,9 +97,7 @@ class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" diff --git a/src/webapi/gen-do-all/jupiter_webapi_gen_do_all/exceptions.py b/src/webapi/gen-do-all/jupiter_webapi_gen_do_all/exceptions.py index d3c7096fb..816f44fbb 100644 --- a/src/webapi/gen-do-all/jupiter_webapi_gen_do_all/exceptions.py +++ b/src/webapi/gen-do-all/jupiter_webapi_gen_do_all/exceptions.py @@ -97,9 +97,7 @@ class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" diff --git a/src/webapi/schedule-external-sync-do-all/jupiter_webapi_schedule_external_sync_do_all/exceptions.py b/src/webapi/schedule-external-sync-do-all/jupiter_webapi_schedule_external_sync_do_all/exceptions.py index 21cedc034..3d4437bf2 100644 --- a/src/webapi/schedule-external-sync-do-all/jupiter_webapi_schedule_external_sync_do_all/exceptions.py +++ b/src/webapi/schedule-external-sync-do-all/jupiter_webapi_schedule_external_sync_do_all/exceptions.py @@ -97,9 +97,7 @@ class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" diff --git a/src/webapi/search-index-backfill-do-all/jupiter_webapi_search_index_backfill_do_all/exceptions.py b/src/webapi/search-index-backfill-do-all/jupiter_webapi_search_index_backfill_do_all/exceptions.py index ea5aea219..b361bbaee 100644 --- a/src/webapi/search-index-backfill-do-all/jupiter_webapi_search_index_backfill_do_all/exceptions.py +++ b/src/webapi/search-index-backfill-do-all/jupiter_webapi_search_index_backfill_do_all/exceptions.py @@ -97,9 +97,7 @@ class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" diff --git a/src/webapi/search-mutation-log-drain-do-all/jupiter_webapi_search_mutation_log_drain_do_all/exceptions.py b/src/webapi/search-mutation-log-drain-do-all/jupiter_webapi_search_mutation_log_drain_do_all/exceptions.py index 4a5143469..d4512378c 100644 --- a/src/webapi/search-mutation-log-drain-do-all/jupiter_webapi_search_mutation_log_drain_do_all/exceptions.py +++ b/src/webapi/search-mutation-log-drain-do-all/jupiter_webapi_search_mutation_log_drain_do_all/exceptions.py @@ -99,9 +99,7 @@ class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" diff --git a/src/webapi/search-mutation-requeue-do-all/jupiter_webapi_search_mutation_requeue_do_all/exceptions.py b/src/webapi/search-mutation-requeue-do-all/jupiter_webapi_search_mutation_requeue_do_all/exceptions.py index 9047ce939..eff98b9b1 100644 --- a/src/webapi/search-mutation-requeue-do-all/jupiter_webapi_search_mutation_requeue_do_all/exceptions.py +++ b/src/webapi/search-mutation-requeue-do-all/jupiter_webapi_search_mutation_requeue_do_all/exceptions.py @@ -99,9 +99,7 @@ class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" diff --git a/src/webapi/srv/jupiter/webapi/exceptions.py b/src/webapi/srv/jupiter/webapi/exceptions.py index f2b8ac1a6..b29f0e09d 100644 --- a/src/webapi/srv/jupiter/webapi/exceptions.py +++ b/src/webapi/srv/jupiter/webapi/exceptions.py @@ -358,9 +358,7 @@ def get_detail(self, exception: TagAlreadyExistsError) -> WebApiError: ) -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" @staticmethod diff --git a/src/webapi/stats-do-all/jupiter_webapi_stats_do_all/exceptions.py b/src/webapi/stats-do-all/jupiter_webapi_stats_do_all/exceptions.py index 7b421d0c6..df7a6c4a4 100644 --- a/src/webapi/stats-do-all/jupiter_webapi_stats_do_all/exceptions.py +++ b/src/webapi/stats-do-all/jupiter_webapi_stats_do_all/exceptions.py @@ -97,9 +97,7 @@ class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" diff --git a/src/webapi/sync-google-user-data-do-all/jupiter_webapi_sync_google_user_data_do_all/exceptions.py b/src/webapi/sync-google-user-data-do-all/jupiter_webapi_sync_google_user_data_do_all/exceptions.py index 7812e4f08..ded1376b0 100644 --- a/src/webapi/sync-google-user-data-do-all/jupiter_webapi_sync_google_user_data_do_all/exceptions.py +++ b/src/webapi/sync-google-user-data-do-all/jupiter_webapi_sync_google_user_data_do_all/exceptions.py @@ -97,9 +97,7 @@ class TagAlreadyExistsHandler(JupiterExceptionHandler[TagAlreadyExistsError]): """Handle tag already exists errors.""" -class EntityIsAlreadyActiveHandler( - JupiterExceptionHandler[EntityIsAlreadyActiveError] -): +class EntityIsAlreadyActiveHandler(JupiterExceptionHandler[EntityIsAlreadyActiveError]): """Handle entity is already active errors.""" diff --git a/src/webui/app/routes/app/public/published.tsx b/src/webui/app/routes/app/public/published.tsx new file mode 100644 index 000000000..1b63937c1 --- /dev/null +++ b/src/webui/app/routes/app/public/published.tsx @@ -0,0 +1,99 @@ +import type { + LoadTopLevelInfoResult, + User, + Workspace, +} from "@jupiter/webapi-client"; +import { DocsHelpSubject } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import type { ShouldRevalidateFunction } from "@remix-run/react"; +import { Outlet } from "@remix-run/react"; +import { CommunityLink } from "@jupiter/core/infra/component/community-link"; +import { DocsHelp } from "@jupiter/core/infra/component/docs-help"; +import { makeRootErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { StandaloneContainer } from "@jupiter/core/infra/component/layout/standalone-container"; +import { SmartAppBar } from "@jupiter/core/infra/component/smart-appbar"; +import { Logo } from "@jupiter/core/infra/component/logo"; +import { Title } from "@jupiter/core/infra/component/title"; +import { TopLevelInfoProvider } from "@jupiter/core/infra/component/top-level-info-provider"; +import { EMPTY_CONTEXT } from "@jupiter/core/infra/top-level-context"; + +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; +import { getGuestApiClient } from "~/api-clients.server"; + +export async function loader({ request }: LoaderFunctionArgs) { + const apiClient = await getGuestApiClient(request); + const response = await apiClient.application.loadTopLevelInfo({}); + + return json({ + userFeatureFlagControls: response.user_feature_flag_controls, + workspaceFeatureFlagControls: response.workspace_feature_flag_controls, + userScoreOverview: response.user_score_overview ?? null, + ...resolvePublishedTopLevelEntities(response), + }); +} + +export const shouldRevalidate: ShouldRevalidateFunction = ({ nextUrl }) => { + return nextUrl.searchParams.has("invalidateTopLevel"); +}; + +export default function Published() { + const loaderData = useLoaderDataSafeForAnimation(); + + return ( + + + + + + + + <CommunityLink /> + + <DocsHelp + size="medium" + subject={DocsHelpSubject.ROOT} + theId="docs-help" + /> + </SmartAppBar> + + <Outlet /> + </StandaloneContainer> + </TopLevelInfoProvider> + ); +} + +export const ErrorBoundary = makeRootErrorBoundary({ + error: () => + `There was an error loading the published page! Please try again!`, +}); + +function resolvePublishedTopLevelEntities(response: LoadTopLevelInfoResult): { + user: User; + workspace: Workspace; +} { + if (response.user && response.workspace) { + return { + user: response.user, + workspace: response.workspace, + }; + } + + return { + user: { + ...EMPTY_CONTEXT.user, + feature_flags: response.default_user_feature_flags, + }, + workspace: { + ...EMPTY_CONTEXT.workspace, + name: response.deafult_workspace_name, + feature_flags: response.default_workspace_feature_flags, + }, + }; +} diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx new file mode 100644 index 000000000..3b7376798 --- /dev/null +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -0,0 +1,62 @@ +import { ApiError, NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { parseEntityLinkStd } from "@jupiter/core/common/entity-link"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; + +import { getGuestApiClient } from "~/api-clients.server"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +function publishedEntityLocation(externalId: string, owner: string): string { + const { theType } = parseEntityLinkStd(owner); + + switch (theType) { + case NamedEntityTag.TODO_TASK: + return `/app/public/published/todo-task/${externalId}`; + default: + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } +} + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.publish.publishEntityLoadByExternalId({ + external_id: externalId, + }); + + return redirect( + publishedEntityLocation(externalId, result.publish_entity.owner), + ); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedEntityRedirect() { + return null; +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => `Could not find published entity ${params.externalId}!`, + error: (params) => + `There was an error loading published entity ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx b/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx new file mode 100644 index 000000000..5e92943d7 --- /dev/null +++ b/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx @@ -0,0 +1,113 @@ +import { ApiError } from "@jupiter/webapi-client"; +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { TodoTaskPropertiesEditor } from "@jupiter/core/todo/components/properties-editor"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.todo.todoTaskLoadPublic({ + external_id: externalId, + }); + + return json({ + todoTask: result.todo_task, + inboxTask: result.inbox_task, + note: result.note ?? null, + aspect: result.aspect, + chapter: result.chapter ?? null, + goal: result.goal ?? null, + tags: result.tags ?? [], + contacts: result.contacts ?? [], + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedTodoTask() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + + return ( + <LeafPanel + key={`published-todo-task-${loaderData.todoTask.ref_id}`} + fakeKey={`published-todo-task-${loaderData.todoTask.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <TodoTaskPropertiesEditor + title="Properties" + topLevelInfo={topLevelInfo} + lifePlan={null} + allAspects={[loaderData.aspect]} + allChapters={loaderData.chapter ? [loaderData.chapter] : []} + allGoals={loaderData.goal ? [loaderData.goal] : []} + allMilestones={[]} + allTags={loaderData.tags} + tags={loaderData.tags} + allContacts={loaderData.contacts} + contacts={loaderData.contacts} + inputsEnabled={false} + todoTask={loaderData.todoTask} + inboxTask={loaderData.inboxTask} + /> + + <SectionCard title="Note"> + {loaderData.note ? ( + <EntityNoteEditor + initialNote={loaderData.note} + inputsEnabled={false} + /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => + `Could not find published todo task ${params.externalId}!`, + error: (params) => + `There was an error loading published todo task ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/workspace/todos/$id.tsx b/src/webui/app/routes/app/workspace/todos/$id.tsx index 571a0a72a..6af7a4488 100644 --- a/src/webui/app/routes/app/workspace/todos/$id.tsx +++ b/src/webui/app/routes/app/workspace/todos/$id.tsx @@ -87,6 +87,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -122,6 +134,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { todoTask: result.todo_task, inboxTask: result.inbox_task, note: result.note, + publishEntity: result.publish_entity, aspect: result.aspect, chapter: result.chapter, goal: result.goal, @@ -305,6 +318,30 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/todos`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/todos/${id}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/todos/${id}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/todos/${id}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -356,6 +393,8 @@ export default function TodoTask() { inputsEnabled={inputsEnabled} entityArchived={loaderData.todoTask.archived} returnLocation="/app/workspace/todos" + publishable + publishEntity={loaderData.publishEntity} > <GlobalError actionResult={actionData} /> <TodoTaskPropertiesEditor From 4288c163f442e8b39ff6ee19b3c5b7b08022cc84 Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Sun, 7 Jun 2026 22:16:44 +0300 Subject: [PATCH 03/21] Exporting vacatiosn now too --- .../api/vacations/vacation_load_public.py | 212 ++++++++++++++++ .../jupiter_webapi_client/models/__init__.py | 2 + .../models/vacation_load_public_args.py | 62 +++++ .../models/vacation_load_result.py | 33 +++ gen/ts/webapi-client/gen/index.ts | 1 + .../gen/models/VacationLoadPublicArgs.ts | 12 + .../gen/models/VacationLoadResult.ts | 2 + .../gen/services/VacationsService.ts | 29 +++ itests/api/todos.test.py | 82 +----- itests/helpers.py | 10 + itests/webui/entities/todos.test.py | 31 +++ itests/webui/entities/vacations.test.py | 37 ++- .../sub/publish/components/publish-panel.tsx | 37 ++- .../common/sub/publish/sub/entity/root.py | 10 +- .../infra/component/layout/leaf-panel.tsx | 1 + .../core/vacations/component/editor.tsx | 127 ++++++++++ src/core/jupiter/core/vacations/root.py | 4 + .../core/vacations/service/__init__.py | 1 + .../jupiter/core/vacations/service/load.py | 97 ++++++++ .../jupiter/core/vacations/use_case/load.py | 75 +----- .../core/vacations/use_case/load_public.py | 79 ++++++ .../app/public/published/$externalId.tsx | 2 + .../public/published/vacation/$externalId.tsx | 110 +++++++++ .../routes/app/workspace/vacations/$id.tsx | 233 ++++++------------ 24 files changed, 970 insertions(+), 319 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/vacations/vacation_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/vacation_load_public_args.py create mode 100644 gen/ts/webapi-client/gen/models/VacationLoadPublicArgs.ts create mode 100644 src/core/jupiter/core/vacations/component/editor.tsx create mode 100644 src/core/jupiter/core/vacations/service/__init__.py create mode 100644 src/core/jupiter/core/vacations/service/load.py create mode 100644 src/core/jupiter/core/vacations/use_case/load_public.py create mode 100644 src/webui/app/routes/app/public/published/vacation/$externalId.tsx diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/vacations/vacation_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/vacations/vacation_load_public.py new file mode 100644 index 000000000..5dd2625e6 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/vacations/vacation_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.vacation_load_public_args import VacationLoadPublicArgs +from ...models.vacation_load_result import VacationLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: VacationLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/vacation-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | VacationLoadResult | None: + if response.status_code == 200: + response_200 = VacationLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | VacationLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: VacationLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | VacationLoadResult]: + """Load a published vacation and its dependent entities by publish external id. + + Args: + body (VacationLoadPublicArgs | Unset): VacationLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | VacationLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: VacationLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | VacationLoadResult | None: + """Load a published vacation and its dependent entities by publish external id. + + Args: + body (VacationLoadPublicArgs | Unset): VacationLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | VacationLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: VacationLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | VacationLoadResult]: + """Load a published vacation and its dependent entities by publish external id. + + Args: + body (VacationLoadPublicArgs | Unset): VacationLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | VacationLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: VacationLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | VacationLoadResult | None: + """Load a published vacation and its dependent entities by publish external id. + + Args: + body (VacationLoadPublicArgs | Unset): VacationLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | VacationLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index fd9ae9ca1..cfd2d3d5e 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -958,6 +958,7 @@ from .vacation_find_result import VacationFindResult from .vacation_find_result_entry import VacationFindResultEntry from .vacation_load_args import VacationLoadArgs +from .vacation_load_public_args import VacationLoadPublicArgs from .vacation_load_result import VacationLoadResult from .vacation_remove_args import VacationRemoveArgs from .vacation_summary import VacationSummary @@ -1945,6 +1946,7 @@ "VacationFindResult", "VacationFindResultEntry", "VacationLoadArgs", + "VacationLoadPublicArgs", "VacationLoadResult", "VacationRemoveArgs", "VacationSummary", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/vacation_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/vacation_load_public_args.py new file mode 100644 index 000000000..474bad0d1 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/vacation_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="VacationLoadPublicArgs") + + +@_attrs_define +class VacationLoadPublicArgs: + """VacationLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + vacation_load_public_args = cls( + external_id=external_id, + ) + + vacation_load_public_args.additional_properties = d + return vacation_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/vacation_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/vacation_load_result.py index 0071540ec..17e4a3a19 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/vacation_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/vacation_load_result.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from ..models.contact import Contact from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_full_days_block import TimeEventFullDaysBlock from ..models.vacation import Vacation @@ -29,6 +30,7 @@ class VacationLoadResult: tags (list[Tag]): contacts (list[Contact]): note (None | Note | Unset): + publish_entity (None | PublishEntity | Unset): """ vacation: Vacation @@ -36,10 +38,12 @@ class VacationLoadResult: tags: list[Tag] contacts: list[Contact] note: None | Note | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.note import Note + from ..models.publish_entity import PublishEntity vacation = self.vacation.to_dict() @@ -63,6 +67,14 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -75,6 +87,8 @@ def to_dict(self) -> dict[str, Any]: ) if note is not UNSET: field_dict["note"] = note + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -82,6 +96,7 @@ def to_dict(self) -> dict[str, Any]: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.contact import Contact from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_full_days_block import TimeEventFullDaysBlock from ..models.vacation import Vacation @@ -122,12 +137,30 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + vacation_load_result = cls( vacation=vacation, time_event_block=time_event_block, tags=tags, contacts=contacts, note=note, + publish_entity=publish_entity, ) vacation_load_result.additional_properties = d diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index ba34e51fe..d55f961ce 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -792,6 +792,7 @@ export type { VacationFindArgs } from './models/VacationFindArgs'; export type { VacationFindResult } from './models/VacationFindResult'; export type { VacationFindResultEntry } from './models/VacationFindResultEntry'; export type { VacationLoadArgs } from './models/VacationLoadArgs'; +export type { VacationLoadPublicArgs } from './models/VacationLoadPublicArgs'; export type { VacationLoadResult } from './models/VacationLoadResult'; export type { VacationName } from './models/VacationName'; export type { VacationRemoveArgs } from './models/VacationRemoveArgs'; diff --git a/gen/ts/webapi-client/gen/models/VacationLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/VacationLoadPublicArgs.ts new file mode 100644 index 000000000..7ac66c40c --- /dev/null +++ b/gen/ts/webapi-client/gen/models/VacationLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * VacationLoadPublic args. + */ +export type VacationLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/VacationLoadResult.ts b/gen/ts/webapi-client/gen/models/VacationLoadResult.ts index 32f57d598..364c9a12b 100644 --- a/gen/ts/webapi-client/gen/models/VacationLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/VacationLoadResult.ts @@ -4,6 +4,7 @@ /* eslint-disable */ import type { Contact } from './Contact'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { Tag } from './Tag'; import type { TimeEventFullDaysBlock } from './TimeEventFullDaysBlock'; import type { Vacation } from './Vacation'; @@ -16,5 +17,6 @@ export type VacationLoadResult = { time_event_block: TimeEventFullDaysBlock; tags: Array<Tag>; contacts: Array<Contact>; + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/services/VacationsService.ts b/gen/ts/webapi-client/gen/services/VacationsService.ts index 958d60059..37004c6b1 100644 --- a/gen/ts/webapi-client/gen/services/VacationsService.ts +++ b/gen/ts/webapi-client/gen/services/VacationsService.ts @@ -8,6 +8,7 @@ import type { VacationCreateResult } from '../models/VacationCreateResult'; import type { VacationFindArgs } from '../models/VacationFindArgs'; import type { VacationFindResult } from '../models/VacationFindResult'; import type { VacationLoadArgs } from '../models/VacationLoadArgs'; +import type { VacationLoadPublicArgs } from '../models/VacationLoadPublicArgs'; import type { VacationLoadResult } from '../models/VacationLoadResult'; import type { VacationRemoveArgs } from '../models/VacationRemoveArgs'; import type { VacationUpdateArgs } from '../models/VacationUpdateArgs'; @@ -127,6 +128,34 @@ export class VacationsService { }, }); } + /** + * Load a published vacation and its dependent entities by publish external id. + * @param requestBody The input data + * @returns VacationLoadResult Successful response + * @throws ApiError + */ + public vacationLoadPublic( + requestBody?: VacationLoadPublicArgs, + ): CancelablePromise<VacationLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/vacation-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * The command for removing a vacation. * @param requestBody The input data diff --git a/itests/api/todos.test.py b/itests/api/todos.test.py index 405e00000..e64dbabc8 100644 --- a/itests/api/todos.test.py +++ b/itests/api/todos.test.py @@ -4,12 +4,6 @@ import pytest import requests -from jupiter_webapi_client.api.publish.publish_entity_activate import ( - sync_detailed as publish_entity_activate_sync, -) -from jupiter_webapi_client.api.publish.publish_entity_create import ( - sync_detailed as publish_entity_create_sync, -) from jupiter_webapi_client.api.test_helper.workspace_set_feature import ( sync_detailed as workspace_set_feature_sync, ) @@ -19,30 +13,13 @@ from jupiter_webapi_client.api.todo.todo_task_create import ( sync_detailed as todo_task_create_sync, ) -from jupiter_webapi_client.api.todo.todo_task_load_public import ( - sync_detailed as todo_task_load_public_sync, -) -from jupiter_webapi_client.client import AuthenticatedClient, Client +from jupiter_webapi_client.client import AuthenticatedClient from jupiter_webapi_client.models.difficulty import Difficulty from jupiter_webapi_client.models.eisen import Eisen -from jupiter_webapi_client.models.publish_entity import PublishEntity -from jupiter_webapi_client.models.publish_entity_activate_args import ( - PublishEntityActivateArgs, -) -from jupiter_webapi_client.models.publish_entity_create_args import ( - PublishEntityCreateArgs, -) -from jupiter_webapi_client.models.publish_entity_create_result import ( - PublishEntityCreateResult, -) from jupiter_webapi_client.models.todo_task import TodoTask from jupiter_webapi_client.models.todo_task_archive_args import TodoTaskArchiveArgs from jupiter_webapi_client.models.todo_task_create_args import TodoTaskCreateArgs from jupiter_webapi_client.models.todo_task_create_result import TodoTaskCreateResult -from jupiter_webapi_client.models.todo_task_load_public_args import ( - TodoTaskLoadPublicArgs, -) -from jupiter_webapi_client.models.todo_task_load_result import TodoTaskLoadResult from jupiter_webapi_client.models.workspace_feature import WorkspaceFeature from jupiter_webapi_client.models.workspace_set_feature_args import ( WorkspaceSetFeatureArgs, @@ -104,35 +81,6 @@ def _headers(api_key: str) -> dict[str, str]: return {"Authorization": f"Bearer {api_key}"} -def _todo_owner(ref_id: str) -> str: - return f"TodoTask:std:{ref_id}" - - -@pytest.fixture() -def create_and_activate_todo_publish( - logged_in_client: AuthenticatedClient, create_todo -): - def _create(name: str) -> tuple[TodoTask, PublishEntity]: - todo_task = create_todo(name) - create_result = publish_entity_create_sync( - client=logged_in_client, - body=PublishEntityCreateArgs( - owner=_todo_owner(todo_task.ref_id), - ), - ) - publish_entity = get_parsed_from_response( - PublishEntityCreateResult, create_result - ).new_publish_entity - activate_result = publish_entity_activate_sync( - client=logged_in_client, - body=PublishEntityActivateArgs(ref_id=publish_entity.ref_id), - ) - assert activate_result.status_code == 200 - return todo_task, publish_entity - - return _create - - def test_api_todo_create(api_url: str, api_key: str) -> None: response = requests.post( f"{api_url}/v1/todos", @@ -161,34 +109,6 @@ def test_api_todo_create(api_url: str, api_key: str) -> None: assert inbox_task["difficulty"] == "medium" -def test_webapi_todo_load_public( - webapi_url: str, create_and_activate_todo_publish -) -> None: - guest_client = Client(base_url=webapi_url) - todo_task, publish_entity = create_and_activate_todo_publish("Public Todo") - - result = todo_task_load_public_sync( - client=guest_client, - body=TodoTaskLoadPublicArgs(external_id=publish_entity.external_id), - ) - assert result.status_code == 200 - - loaded = get_parsed_from_response(TodoTaskLoadResult, result) - assert loaded.todo_task.ref_id == todo_task.ref_id - assert loaded.todo_task.name == "Public Todo" - assert loaded.inbox_task.name == "Public Todo" - - -def test_webapi_todo_load_public_not_found(webapi_url: str) -> None: - guest_client = Client(base_url=webapi_url) - - result = todo_task_load_public_sync( - client=guest_client, - body=TodoTaskLoadPublicArgs(external_id="00000000-0000-4000-8000-000000000000"), - ) - assert result.status_code == 404 - - def test_api_todo_load(api_url: str, api_key: str, create_todo) -> None: created = create_todo("Load Todo") diff --git a/itests/helpers.py b/itests/helpers.py index a2fe24d46..01273ef8d 100644 --- a/itests/helpers.py +++ b/itests/helpers.py @@ -51,6 +51,16 @@ def type_entity_note_editor_and_wait_for_save(page: Page, text: str) -> None: ) +def open_leaf_publish_panel(page: Page, publish_section_id: str) -> None: + """Open the publish panel in a leaf entity view.""" + publish_panel = page.locator(f"#{publish_section_id}") + if publish_panel.is_visible(): + return + + page.locator("button[id='leaf-entity-publish']").click() + publish_panel.wait_for(state="visible") + + def get_parsed_from_response(clazz: Type[T], response: Response[S]) -> T: """Get the parsed response as a specific type.""" if response.status_code != 200: diff --git a/itests/webui/entities/todos.test.py b/itests/webui/entities/todos.test.py index c1cf18270..5f244e6bc 100644 --- a/itests/webui/entities/todos.test.py +++ b/itests/webui/entities/todos.test.py @@ -24,6 +24,7 @@ from itests.helpers import ( get_parsed_from_response, + open_leaf_publish_panel, type_entity_note_editor_and_wait_for_save, ) @@ -173,3 +174,33 @@ def test_webui_todo_archive(page: Page, create_todo) -> None: expect(page.locator('input[name="name"]')).to_be_disabled() expect(page.locator("button[id='todo-update']")).to_be_disabled() expect(page.locator("button[id='todo-create-note']")).to_be_disabled() + + +def test_webui_todo_publish_and_view_public(page: Page, create_todo) -> None: + todo = create_todo("Published Todo") + page.goto(f"/app/workspace/todos/{todo.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "TodoTask-publish") + page.locator("button[id='TodoTask-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/todos/{todo.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "TodoTask-publish") + expect(page.locator("#TodoTask-publish")).to_contain_text("draft") + + page.locator("button[id='TodoTask-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/todos/{todo.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "TodoTask-publish") + expect(page.locator("#TodoTask-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/todo-task/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="name"]')).to_have_value("Published Todo") diff --git a/itests/webui/entities/vacations.test.py b/itests/webui/entities/vacations.test.py index c818562a8..ad3469ff2 100644 --- a/itests/webui/entities/vacations.test.py +++ b/itests/webui/entities/vacations.test.py @@ -22,6 +22,7 @@ from itests.helpers import ( get_parsed_from_response, + open_leaf_publish_panel, type_entity_note_editor_and_wait_for_save, ) @@ -54,8 +55,8 @@ def _create_vacation( client=logged_in_client, body=VacationCreateArgs( name=name, - start_date=f"2024-{start_month}-{start_day}", - end_date=f"2024-{end_month}-{end_day}", + start_date=f"2024-{start_month:02d}-{start_day:02d}", + end_date=f"2024-{end_month:02d}-{end_day:02d}", ), ) return get_parsed_from_response(VacationCreateResult, result).new_vacation @@ -189,3 +190,35 @@ def test_webui_vacation_archive(page: Page, create_vacation) -> None: entity_id = page.url.split("/")[-1] expect(page.locator(f"#vacation-{entity_id}")).to_have_count(0) + + +def test_webui_vacation_publish_and_view_public(page: Page, create_vacation) -> None: + vacation = create_vacation("Published Vacation", 7, 1, 7, 14) + page.goto(f"/app/workspace/vacations/{vacation.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Vacation-publish") + page.locator("button[id='Vacation-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/vacations/{vacation.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Vacation-publish") + expect(page.locator("#Vacation-publish")).to_contain_text("draft") + + page.locator("button[id='Vacation-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/vacations/{vacation.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Vacation-publish") + expect(page.locator("#Vacation-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/vacation/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="name"]')).to_have_value("Published Vacation") + expect(page.locator('input[name="startDate"]')).to_have_value("2024-07-01") + expect(page.locator('input[name="endDate"]')).to_have_value("2024-07-14") diff --git a/src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx b/src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx index 780a43d85..b30ef38c8 100644 --- a/src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx +++ b/src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx @@ -13,7 +13,7 @@ import { Stack, Typography, } from "@mui/material"; -import { useContext } from "react"; +import { useContext, useState } from "react"; import { entityLinkStd } from "#/core/common/entity-link"; import { ServicePropertiesContext } from "#/core/config-client"; @@ -34,6 +34,7 @@ interface PublishPanelProps { export function PublishPanel(props: PublishPanelProps) { const serviceProperties = useContext(ServicePropertiesContext); + const [hasCopiedPublicUrl, setHasCopiedPublicUrl] = useState(false); const sectionId = `${props.entityType}-publish`; const publishOwner = entityLinkStd(props.entityType, props.entityRefId); const publicUrl = @@ -42,6 +43,11 @@ export function PublishPanel(props: PublishPanelProps) { : ""; const isActive = props.publishEntity?.status === PublishEntityStatus.ACTIVE; + async function copyPublicUrl() { + await navigator.clipboard.writeText(publicUrl); + setHasCopiedPublicUrl(true); + } + return ( <SectionCard id={sectionId} @@ -100,16 +106,25 @@ export function PublishPanel(props: PublishPanelProps) { value={publicUrl} endAdornment={ <InputAdornment position="end"> - <Button - component="a" - href={publicUrl} - target="_blank" - rel="noreferrer" - variant="outlined" - disabled={!isActive} - > - View - </Button> + <Stack direction="row" spacing={1}> + <Button + variant="outlined" + onClick={copyPublicUrl} + disabled={!isActive || hasCopiedPublicUrl} + > + {hasCopiedPublicUrl ? "Copied" : "Copy"} + </Button> + <Button + component="a" + href={publicUrl} + target="_blank" + rel="noreferrer" + variant="outlined" + disabled={!isActive} + > + View + </Button> + </Stack> </InputAdornment> } /> diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index 4470f5e19..1c1178463 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -26,8 +26,7 @@ # Allowed ``EntityLink.the_type`` values for shareable :class:`PublishEntity` owners. ALLOWED_PUBLISH_OWNER_TYPES: Final[frozenset[str]] = frozenset( { - NamedEntityTag.TODO_TASK.value, - NamedEntityTag.WORKING_MEM.value, + NamedEntityTag.TODO_TASK.value, # done NamedEntityTag.TIME_PLAN.value, NamedEntityTag.SCHEDULE_STREAM.value, NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, @@ -38,12 +37,7 @@ NamedEntityTag.DOC.value, NamedEntityTag.DIR.value, NamedEntityTag.JOURNAL.value, - NamedEntityTag.CHAPTER.value, - NamedEntityTag.GOAL.value, - NamedEntityTag.MILESTONE.value, - NamedEntityTag.VISION.value, - NamedEntityTag.VACATION.value, - NamedEntityTag.ASPECT.value, + NamedEntityTag.VACATION.value, # done NamedEntityTag.SMART_LIST.value, NamedEntityTag.SMART_LIST_ITEM.value, NamedEntityTag.METRIC.value, diff --git a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx index 766ae75ab..32d8f3266 100644 --- a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx +++ b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx @@ -393,6 +393,7 @@ export function LeafPanel(props: PropsWithChildren<LeafPanelProps>) { )} {hasPublish && ( <IconButton + id="leaf-entity-publish" onClick={() => { setShowHistory(false); setShowPublish((p) => !p); diff --git a/src/core/jupiter/core/vacations/component/editor.tsx b/src/core/jupiter/core/vacations/component/editor.tsx new file mode 100644 index 000000000..611e4efc9 --- /dev/null +++ b/src/core/jupiter/core/vacations/component/editor.tsx @@ -0,0 +1,127 @@ +import type { Contact, Tag, Vacation } from "@jupiter/webapi-client"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; +import { aDateToDate } from "#/core/common/adate"; +import { entityLinkStd } from "#/core/common/entity-link"; +import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; +import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; +import type { ActionResult } from "#/core/infra/action-result"; +import { FieldError } from "#/core/infra/component/errors"; +import { + ActionSingle, + SectionActions, +} from "#/core/infra/component/section-actions"; +import { SectionCard } from "#/core/infra/component/section-card"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import type { TopLevelInfo } from "#/core/infra/top-level-context"; + +interface VacationEditorProps { + vacation: Vacation; + tags: Array<Tag>; + contacts: Array<Contact>; + allTags: Array<Tag>; + allContacts: Array<Contact>; + inputsEnabled: boolean; + topLevelInfo: TopLevelInfo; + actionResult?: ActionResult<unknown>; +} + +export function VacationEditor(props: VacationEditorProps) { + const isBigScreen = useBigScreen(); + const { vacation, tags, contacts, allTags, allContacts } = props; + + return ( + <SectionCard + title="Properties" + actions={ + props.inputsEnabled ? ( + <SectionActions + id="vacation-update" + topLevelInfo={props.topLevelInfo} + inputsEnabled={props.inputsEnabled} + actions={[ + ActionSingle({ + id: "vacation-update", + text: "Save", + value: "update", + highlight: true, + }), + ]} + /> + ) : undefined + } + > + <Stack direction="row" spacing={1}> + <FormControl fullWidth sx={{ flexGrow: 3 }}> + <InputLabel id="name">Name</InputLabel> + <OutlinedInput + label="name" + name="name" + readOnly={!props.inputsEnabled} + disabled={!props.inputsEnabled} + defaultValue={vacation.name} + /> + <FieldError actionResult={props.actionResult} fieldName="/name" /> + </FormControl> + </Stack> + + <Stack direction={isBigScreen ? "row" : "column"} useFlexGap spacing={1}> + <FormControl sx={{ flexGrow: 2 }}> + <TagsEditor + name="tags" + aloneOnLine + allTags={allTags} + defaultValue={tags.map((tag) => tag.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.VACATION, vacation.ref_id)} + /> + </FormControl> + + <FormControl sx={{ flexGrow: 2 }}> + <ContactsEditor + name="contacts_names" + aloneOnLine + allContacts={allContacts} + defaultValue={contacts.map((contact) => contact.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.VACATION, vacation.ref_id)} + /> + </FormControl> + </Stack> + + <FormControl fullWidth> + <InputLabel id="startDate" shrink> + Start Date + </InputLabel> + <OutlinedInput + type="date" + notched + label="startDate" + defaultValue={aDateToDate(vacation.start_date).toFormat("yyyy-MM-dd")} + name="startDate" + readOnly={!props.inputsEnabled} + disabled={!props.inputsEnabled} + /> + + <FieldError actionResult={props.actionResult} fieldName="/start_date" /> + </FormControl> + + <FormControl fullWidth> + <InputLabel id="endDate" shrink> + End Date + </InputLabel> + <OutlinedInput + type="date" + notched + label="endDate" + defaultValue={aDateToDate(vacation.end_date).toFormat("yyyy-MM-dd")} + name="endDate" + readOnly={!props.inputsEnabled} + disabled={!props.inputsEnabled} + /> + + <FieldError actionResult={props.actionResult} fieldName="/end_date" /> + </FormControl> + </SectionCard> + ); +} diff --git a/src/core/jupiter/core/vacations/root.py b/src/core/jupiter/core/vacations/root.py index f96de1d8c..164aa71bb 100644 --- a/src/core/jupiter/core/vacations/root.py +++ b/src/core/jupiter/core/vacations/root.py @@ -3,6 +3,7 @@ import typing from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( TimeEventFullDaysBlock, @@ -39,6 +40,9 @@ class Vacation(LeafEntity): TagLink, owner=IsEntityLinkStd(NamedEntityTag.VACATION.value) ) note = OwnsAtMostOne(Note, owner=IsEntityLinkStd(NamedEntityTag.VACATION.value)) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.VACATION.value) + ) time_event_block = OwnsOne( TimeEventFullDaysBlock, owner=IsEntityLinkStd(NamedEntityTag.VACATION.value), diff --git a/src/core/jupiter/core/vacations/service/__init__.py b/src/core/jupiter/core/vacations/service/__init__.py new file mode 100644 index 000000000..e273ca1a6 --- /dev/null +++ b/src/core/jupiter/core/vacations/service/__init__.py @@ -0,0 +1 @@ +"""Services for vacations.""" diff --git a/src/core/jupiter/core/vacations/service/load.py b/src/core/jupiter/core/vacations/service/load.py new file mode 100644 index 000000000..1d102fd8c --- /dev/null +++ b/src/core/jupiter/core/vacations/service/load.py @@ -0,0 +1,97 @@ +"""Shared service for loading a vacation and its dependent entities.""" + +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( + TimeEventFullDaysBlock, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.vacations.root import Vacation +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result +from jupiter.framework.utils.generic_loader import generic_loader + + +@use_case_result +class VacationLoadResult(UseCaseResultBase): + """VacationLoadResult.""" + + vacation: Vacation + note: Note | None + time_event_block: TimeEventFullDaysBlock + tags: list[Tag] + contacts: list[Contact] + publish_entity: PublishEntity | None + + +class VacationLoadService: + """Shared service for loading a vacation and its dependent entities.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + vacation: Vacation, + *, + allow_archived: bool = False, + ) -> VacationLoadResult: + """Load a vacation together with the entities that hang off it.""" + vacation, note, time_event_block = await generic_loader( + uow, + Vacation, + vacation.ref_id, + Vacation.note, + Vacation.time_event_block, + allow_archived=allow_archived, + ) + + publish_entity = await uow.get(PublishEntityRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.VACATION.value, vacation.ref_id), + allow_archived=allow_archived, + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std(NamedEntityTag.VACATION.value, vacation.ref_id), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace_ref_id, + ) + contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.VACATION.value, vacation.ref_id), + ) + if contact_link is not None: + contacts = await uow.get_for(Contact).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + ref_id=contact_link.contacts_ref_ids, + ) + else: + contacts = [] + + return VacationLoadResult( + vacation=vacation, + note=note, + time_event_block=time_event_block, + tags=tags, + contacts=contacts, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/vacations/use_case/load.py b/src/core/jupiter/core/vacations/use_case/load.py index f34e93970..6f8177de6 100644 --- a/src/core/jupiter/core/vacations/use_case/load.py +++ b/src/core/jupiter/core/vacations/use_case/load.py @@ -1,34 +1,23 @@ """Use case for loading a particular vacation.""" -from jupiter.core.common.sub.contacts.root import ContactDomain -from jupiter.core.common.sub.contacts.sub.contact.root import Contact -from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository -from jupiter.core.common.sub.notes.root import Note -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository -from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( - TimeEventFullDaysBlock, -) from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.core.vacations.root import Vacation +from jupiter.core.vacations.service.load import VacationLoadResult, VacationLoadService from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import ( readonly_use_case, ) from jupiter.framework.use_case_io import ( UseCaseArgsBase, - UseCaseResultBase, use_case_args, - use_case_result, ) -from jupiter.framework.utils.generic_loader import generic_loader + +__all__ = ["VacationLoadArgs", "VacationLoadResult", "VacationLoadUseCase"] @use_case_args @@ -39,17 +28,6 @@ class VacationLoadArgs(UseCaseArgsBase): allow_archived: bool | None -@use_case_result -class VacationLoadResult(UseCaseResultBase): - """VacationLoadResult.""" - - vacation: Vacation - note: Note | None - time_event_block: TimeEventFullDaysBlock - tags: list[Tag] - contacts: list[Contact] - - @readonly_use_case(WorkspaceFeature.VACATIONS) class VacationLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[VacationLoadArgs, VacationLoadResult] @@ -64,46 +42,15 @@ async def _perform_transactional_read( ) -> VacationLoadResult: """Execute the command's action.""" allow_archived = args.allow_archived or False + workspace = context.workspace - vacation, note, time_event_block = await generic_loader( - uow, - Vacation, - args.ref_id, - Vacation.note, - Vacation.time_event_block, - allow_archived=allow_archived, + vacation = await uow.get_for(Vacation).load_by_id( + args.ref_id, allow_archived=allow_archived ) - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.VACATION.value, vacation.ref_id), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - contact_domain = await uow.get_for(ContactDomain).load_by_parent( - context.workspace.ref_id, - ) - contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.VACATION.value, vacation.ref_id), - ) - if contact_link is not None: - contacts = await uow.get_for(Contact).find_all_generic( - parent_ref_id=contact_domain.ref_id, - allow_archived=False, - ref_id=contact_link.contacts_ref_ids, - ) - else: - contacts = [] - - return VacationLoadResult( - vacation=vacation, - note=note, - time_event_block=time_event_block, - tags=tags, - contacts=contacts, + return await VacationLoadService().do_it( + uow, + workspace.ref_id, + vacation, + allow_archived=allow_archived, ) diff --git a/src/core/jupiter/core/vacations/use_case/load_public.py b/src/core/jupiter/core/vacations/use_case/load_public.py new file mode 100644 index 000000000..d76fc0945 --- /dev/null +++ b/src/core/jupiter/core/vacations/use_case/load_public.py @@ -0,0 +1,79 @@ +"""Guest readonly use case for loading a published vacation by external id.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.vacations.collection import VacationCollection +from jupiter.core.vacations.root import Vacation +from jupiter.core.vacations.service.load import VacationLoadResult, VacationLoadService +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import ( + UseCaseArgsBase, + use_case_args, +) + + +@use_case_args +class VacationLoadPublicArgs(UseCaseArgsBase): + """VacationLoadPublic args.""" + + external_id: PublishExternalId + + +class VacationLoadPublicUseCase( + JupiterGuestReadonlyUseCase[VacationLoadPublicArgs, VacationLoadResult] +): + """Load a published vacation and its dependent entities by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: VacationLoadPublicArgs, + ) -> VacationLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.VACATION.value: + raise InputValidationError( + "The publish entity does not refer to a vacation." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + vacation_collection = await uow.get_for(VacationCollection).load_by_parent( + publish_domain.workspace.ref_id + ) + vacation = await uow.get_for(Vacation).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if vacation.parent_ref_id != vacation_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace vacation." + ) + + return await VacationLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + vacation, + allow_archived=False, + ) diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx index 3b7376798..004cbdf3b 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -19,6 +19,8 @@ function publishedEntityLocation(externalId: string, owner: string): string { switch (theType) { case NamedEntityTag.TODO_TASK: return `/app/public/published/todo-task/${externalId}`; + case NamedEntityTag.VACATION: + return `/app/public/published/vacation/${externalId}`; default: throw new Response(ReasonPhrases.NOT_FOUND, { status: StatusCodes.NOT_FOUND, diff --git a/src/webui/app/routes/app/public/published/vacation/$externalId.tsx b/src/webui/app/routes/app/public/published/vacation/$externalId.tsx new file mode 100644 index 000000000..458ac5a71 --- /dev/null +++ b/src/webui/app/routes/app/public/published/vacation/$externalId.tsx @@ -0,0 +1,110 @@ +import { ApiError } from "@jupiter/webapi-client"; +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { TimeEventFullDaysBlockStack } from "@jupiter/core/common/sub/time_events/sub/full_days_block/component/stack"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { VacationEditor } from "@jupiter/core/vacations/component/editor"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.vacations.vacationLoadPublic({ + external_id: externalId, + }); + + return json({ + vacation: result.vacation, + note: result.note ?? null, + timeEventBlock: result.time_event_block, + tags: result.tags ?? [], + contacts: result.contacts ?? [], + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedVacation() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const { vacation, note, timeEventBlock, tags, contacts } = loaderData; + + const timeEventBlockEntry = { + time_event: timeEventBlock, + entry: { + vacation: vacation, + time_event: timeEventBlock, + }, + }; + + return ( + <LeafPanel + key={`published-vacation-${vacation.ref_id}`} + fakeKey={`published-vacation-${vacation.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <VacationEditor + vacation={vacation} + tags={tags} + contacts={contacts} + allTags={tags} + allContacts={contacts} + inputsEnabled={false} + topLevelInfo={topLevelInfo} + /> + + <SectionCard title="Note"> + {note ? ( + <EntityNoteEditor initialNote={note} inputsEnabled={false} /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => + `Could not find published vacation ${params.externalId}!`, + error: (params) => + `There was an error loading published vacation ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/workspace/vacations/$id.tsx b/src/webui/app/routes/app/workspace/vacations/$id.tsx index a2f38252d..bf0258410 100644 --- a/src/webui/app/routes/app/workspace/vacations/$id.tsx +++ b/src/webui/app/routes/app/workspace/vacations/$id.tsx @@ -5,7 +5,6 @@ import { Tag, WorkspaceFeature, } from "@jupiter/webapi-client"; -import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; @@ -14,25 +13,21 @@ import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseForm, parseParams } from "zodix"; -import { aDateToDate } from "@jupiter/core/common/adate"; import { isWorkspaceFeatureAvailable } from "@jupiter/core/workspaces/root"; import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; -import { FieldError, GlobalError } from "@jupiter/core/infra/component/errors"; +import { GlobalError } from "@jupiter/core/infra/component/errors"; import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; import { TimeEventFullDaysBlockStack } from "@jupiter/core/common/sub/time_events/sub/full_days_block/component/stack"; import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; -import { SectionCard } from "@jupiter/core/infra/component/section-card"; import { ActionSingle, SectionActions, } from "@jupiter/core/infra/component/section-actions"; -import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; -import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; -import { entityLinkStd } from "@jupiter/core/common/entity-link"; -import { useBigScreen } from "@jupiter/core/infra/component/use-big-screen"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { VacationEditor } from "@jupiter/core/vacations/component/editor"; import { noteStdOwner } from "#/core/common/sub/notes/note-std-owner"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -59,6 +54,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -87,12 +94,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { note: result.note, timeEventBlock: result.time_event_block, tags: result.tags, - contacts: - ( - result as { - contacts?: Array<Contact>; - } - ).contacts ?? [], + contacts: result.contacts ?? [], + publishEntity: result.publish_entity ?? null, allTags: allTags.tags as Array<Tag>, allContacts: allContacts.contacts as Array<Contact>, }); @@ -158,6 +161,30 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/vacations`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/vacations/${id}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/vacations/${id}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/vacations/${id}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -177,144 +204,58 @@ export const shouldRevalidate: ShouldRevalidateFunction = standardShouldRevalidate; export default function Vacation() { - const { - vacation, - note, - timeEventBlock, - tags, - contacts, - allTags, - allContacts, - } = useLoaderDataSafeForAnimation<typeof loader>(); + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const actionData = useActionData<typeof action>(); const navigation = useNavigation(); const topLevelInfo = useContext(TopLevelInfoContext); - const isBigScreen = useBigScreen(); - const inputsEnabled = navigation.state === "idle" && !vacation.archived; + const inputsEnabled = + navigation.state === "idle" && !loaderData.vacation.archived; const timeEventBlockEntry = { - time_event: timeEventBlock, + time_event: loaderData.timeEventBlock, entry: { - vacation: vacation, - time_event: timeEventBlock, + vacation: loaderData.vacation, + time_event: loaderData.timeEventBlock, }, }; return ( <LeafPanel - key={`vacation-${vacation.ref_id}`} + key={`vacation-${loaderData.vacation.ref_id}`} entityType={NamedEntityTag.VACATION} - entityRefId={vacation.ref_id} - fakeKey={`vacation-${vacation.ref_id}`} + entityRefId={loaderData.vacation.ref_id} + fakeKey={`vacation-${loaderData.vacation.ref_id}`} showArchiveAndRemoveButton inputsEnabled={inputsEnabled} - entityArchived={vacation.archived} + entityArchived={loaderData.vacation.archived} returnLocation="/app/workspace/vacations" + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <GlobalError actionResult={actionData} /> - <SectionCard - title="Properties" - actions={ - <SectionActions - id="vacation-update" - topLevelInfo={topLevelInfo} - inputsEnabled={inputsEnabled} - actions={[ - ActionSingle({ - id: "vacation-update", - text: "Save", - value: "update", - highlight: true, - }), - ]} - /> - } - > - <Stack direction="row" spacing={1}> - <FormControl fullWidth sx={{ flexGrow: 3 }}> - <InputLabel id="name">Name</InputLabel> - <OutlinedInput - label="name" - name="name" - readOnly={!inputsEnabled} - disabled={!inputsEnabled} - defaultValue={vacation.name} - /> - <FieldError actionResult={actionData} fieldName="/name" /> - </FormControl> - </Stack> - - <Stack - direction={isBigScreen ? "row" : "column"} - useFlexGap - spacing={1} - > - {allTags && tags && ( - <FormControl sx={{ flexGrow: 2 }}> - <TagsEditor - name="tags" - aloneOnLine - allTags={allTags} - defaultValue={tags.map((tag: Tag) => tag.ref_id)} - inputsEnabled={inputsEnabled} - owner={entityLinkStd(NamedEntityTag.VACATION, vacation.ref_id)} - /> - </FormControl> - )} - - {allContacts && contacts && ( - <FormControl sx={{ flexGrow: 2 }}> - <ContactsEditor - name="contacts_names" - aloneOnLine - allContacts={allContacts} - defaultValue={contacts.map( - (contact: Contact) => contact.ref_id, - )} - inputsEnabled={inputsEnabled} - owner={entityLinkStd(NamedEntityTag.VACATION, vacation.ref_id)} - /> - </FormControl> - )} - </Stack> - - <FormControl fullWidth> - <InputLabel id="startDate" shrink> - Start Date - </InputLabel> - <OutlinedInput - type="date" - notched - label="startDate" - defaultValue={aDateToDate(vacation.start_date).toFormat( - "yyyy-MM-dd", - )} - name="startDate" - readOnly={!inputsEnabled} - disabled={!inputsEnabled} - /> + <VacationEditor + vacation={loaderData.vacation} + tags={loaderData.tags} + contacts={loaderData.contacts} + allTags={loaderData.allTags} + allContacts={loaderData.allContacts} + inputsEnabled={inputsEnabled} + topLevelInfo={topLevelInfo} + actionResult={actionData} + /> - <FieldError actionResult={actionData} fieldName="/start_date" /> - </FormControl> - - <FormControl fullWidth> - <InputLabel id="endDate" shrink> - End Date - </InputLabel> - <OutlinedInput - type="date" - notched - label="endDate" - defaultValue={aDateToDate(vacation.end_date).toFormat("yyyy-MM-dd")} - name="endDate" - readOnly={!inputsEnabled} - disabled={!inputsEnabled} - /> - - <FieldError actionResult={actionData} fieldName="/end_date" /> - </FormControl> - </SectionCard> + {isWorkspaceFeatureAvailable( + topLevelInfo.workspace, + WorkspaceFeature.SCHEDULE, + ) && ( + <TimeEventFullDaysBlockStack + topLevelInfo={topLevelInfo} + inputsEnabled={inputsEnabled} + title="Time Events" + entries={[timeEventBlockEntry]} + /> + )} <SectionCard title="Note" @@ -329,33 +270,19 @@ export default function Vacation() { text: "Create Note", value: "create-note", highlight: false, - disabled: note !== null, + disabled: loaderData.note !== null, }), ]} /> } > - {note && ( - <> - <EntityNoteEditor - initialNote={note} - inputsEnabled={inputsEnabled} - /> - </> + {loaderData.note && ( + <EntityNoteEditor + initialNote={loaderData.note} + inputsEnabled={inputsEnabled} + /> )} </SectionCard> - - {isWorkspaceFeatureAvailable( - topLevelInfo.workspace, - WorkspaceFeature.SCHEDULE, - ) && ( - <TimeEventFullDaysBlockStack - topLevelInfo={topLevelInfo} - inputsEnabled={inputsEnabled} - title="Time Events" - entries={[timeEventBlockEntry]} - /> - )} </LeafPanel> ); } From 8ca67d5985b6d08a3d6a6794eac1384379ad8fa7 Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Sun, 7 Jun 2026 22:27:02 +0300 Subject: [PATCH 04/21] Adding journals too --- .../api/journals/journal_load_public.py | 212 ++++++++++++++++++ .../jupiter_webapi_client/models/__init__.py | 2 + .../models/journal_load_public_args.py | 62 +++++ .../models/journal_load_result.py | 35 ++- gen/ts/webapi-client/gen/index.ts | 1 + .../gen/models/JournalLoadPublicArgs.ts | 12 + .../gen/models/JournalLoadResult.ts | 4 +- .../gen/services/JournalsService.ts | 29 +++ itests/webui/entities/journals.test.py | 37 ++- .../common/sub/publish/sub/entity/root.py | 2 +- .../infra/component/layout/leaf-panel.tsx | 18 +- .../core/journals/component/editor.tsx | 109 +++++++++ src/core/jupiter/core/journals/root.py | 4 + .../jupiter/core/journals/service/__init__.py | 1 + .../jupiter/core/journals/service/load.py | 110 +++++++++ .../jupiter/core/journals/use_case/load.py | 85 +------ .../core/journals/use_case/load_public.py | 80 +++++++ .../app/public/published/$externalId.tsx | 2 + .../public/published/journal/$externalId.tsx | 110 +++++++++ .../app/routes/app/workspace/journals/$id.tsx | 134 +++++------ 20 files changed, 877 insertions(+), 172 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/journals/journal_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/journal_load_public_args.py create mode 100644 gen/ts/webapi-client/gen/models/JournalLoadPublicArgs.ts create mode 100644 src/core/jupiter/core/journals/component/editor.tsx create mode 100644 src/core/jupiter/core/journals/service/__init__.py create mode 100644 src/core/jupiter/core/journals/service/load.py create mode 100644 src/core/jupiter/core/journals/use_case/load_public.py create mode 100644 src/webui/app/routes/app/public/published/journal/$externalId.tsx diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/journals/journal_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/journals/journal_load_public.py new file mode 100644 index 000000000..54e5a634f --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/journals/journal_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.journal_load_public_args import JournalLoadPublicArgs +from ...models.journal_load_result import JournalLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: JournalLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/journal-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | JournalLoadResult | None: + if response.status_code == 200: + response_200 = JournalLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | JournalLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: JournalLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | JournalLoadResult]: + """Load a published journal and its dependent entities by publish external id. + + Args: + body (JournalLoadPublicArgs | Unset): JournalLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | JournalLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: JournalLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | JournalLoadResult | None: + """Load a published journal and its dependent entities by publish external id. + + Args: + body (JournalLoadPublicArgs | Unset): JournalLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | JournalLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: JournalLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | JournalLoadResult]: + """Load a published journal and its dependent entities by publish external id. + + Args: + body (JournalLoadPublicArgs | Unset): JournalLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | JournalLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: JournalLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | JournalLoadResult | None: + """Load a published journal and its dependent entities by publish external id. + + Args: + body (JournalLoadPublicArgs | Unset): JournalLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | JournalLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index cfd2d3d5e..c174c4d23 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -408,6 +408,7 @@ from .journal_load_args import JournalLoadArgs from .journal_load_for_date_and_period_args import JournalLoadForDateAndPeriodArgs from .journal_load_for_date_and_period_result import JournalLoadForDateAndPeriodResult +from .journal_load_public_args import JournalLoadPublicArgs from .journal_load_result import JournalLoadResult from .journal_load_settings_args import JournalLoadSettingsArgs from .journal_load_settings_result import JournalLoadSettingsResult @@ -1414,6 +1415,7 @@ "JournalLoadArgs", "JournalLoadForDateAndPeriodArgs", "JournalLoadForDateAndPeriodResult", + "JournalLoadPublicArgs", "JournalLoadResult", "JournalLoadSettingsArgs", "JournalLoadSettingsResult", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/journal_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/journal_load_public_args.py new file mode 100644 index 000000000..c2f514d24 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/journal_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="JournalLoadPublicArgs") + + +@_attrs_define +class JournalLoadPublicArgs: + """JournalLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + journal_load_public_args = cls( + external_id=external_id, + ) + + journal_load_public_args.additional_properties = d + return journal_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/journal_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/journal_load_result.py index d63bf129d..2af610237 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/journal_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/journal_load_result.py @@ -13,6 +13,7 @@ from ..models.journal import Journal from ..models.journal_stats import JournalStats from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag @@ -21,7 +22,7 @@ @_attrs_define class JournalLoadResult: - """Result. + """JournalLoadResult. Attributes: journal (Journal): A journal for a particular range. @@ -30,6 +31,7 @@ class JournalLoadResult: journal_stats (JournalStats): Stats about a journal. sub_period_journals (list[Journal]): writing_task (InboxTask | None | Unset): + publish_entity (None | PublishEntity | Unset): """ journal: Journal @@ -38,10 +40,12 @@ class JournalLoadResult: journal_stats: JournalStats sub_period_journals: list[Journal] writing_task: InboxTask | None | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.inbox_task import InboxTask + from ..models.publish_entity import PublishEntity journal = self.journal.to_dict() @@ -67,6 +71,14 @@ def to_dict(self) -> dict[str, Any]: else: writing_task = self.writing_task + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -80,6 +92,8 @@ def to_dict(self) -> dict[str, Any]: ) if writing_task is not UNSET: field_dict["writing_task"] = writing_task + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -89,6 +103,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.journal import Journal from ..models.journal_stats import JournalStats from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag d = dict(src_dict) @@ -129,6 +144,23 @@ def _parse_writing_task(data: object) -> InboxTask | None | Unset: writing_task = _parse_writing_task(d.pop("writing_task", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + journal_load_result = cls( journal=journal, tags=tags, @@ -136,6 +168,7 @@ def _parse_writing_task(data: object) -> InboxTask | None | Unset: journal_stats=journal_stats, sub_period_journals=sub_period_journals, writing_task=writing_task, + publish_entity=publish_entity, ) journal_load_result.additional_properties = d diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index d55f961ce..f227f8f15 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -341,6 +341,7 @@ export { JournalGenerationApproach } from './models/JournalGenerationApproach'; export type { JournalLoadArgs } from './models/JournalLoadArgs'; export type { JournalLoadForDateAndPeriodArgs } from './models/JournalLoadForDateAndPeriodArgs'; export type { JournalLoadForDateAndPeriodResult } from './models/JournalLoadForDateAndPeriodResult'; +export type { JournalLoadPublicArgs } from './models/JournalLoadPublicArgs'; export type { JournalLoadResult } from './models/JournalLoadResult'; export type { JournalLoadSettingsArgs } from './models/JournalLoadSettingsArgs'; export type { JournalLoadSettingsResult } from './models/JournalLoadSettingsResult'; diff --git a/gen/ts/webapi-client/gen/models/JournalLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/JournalLoadPublicArgs.ts new file mode 100644 index 000000000..4ab97527f --- /dev/null +++ b/gen/ts/webapi-client/gen/models/JournalLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * JournalLoadPublic args. + */ +export type JournalLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/JournalLoadResult.ts b/gen/ts/webapi-client/gen/models/JournalLoadResult.ts index bfa369a74..6cfc8c846 100644 --- a/gen/ts/webapi-client/gen/models/JournalLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/JournalLoadResult.ts @@ -6,9 +6,10 @@ import type { InboxTask } from './InboxTask'; import type { Journal } from './Journal'; import type { JournalStats } from './JournalStats'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { Tag } from './Tag'; /** - * Result. + * JournalLoadResult. */ export type JournalLoadResult = { journal: Journal; @@ -17,5 +18,6 @@ export type JournalLoadResult = { journal_stats: JournalStats; writing_task?: (InboxTask | null); sub_period_journals: Array<Journal>; + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/services/JournalsService.ts b/gen/ts/webapi-client/gen/services/JournalsService.ts index 9b069c227..bc073bc5e 100644 --- a/gen/ts/webapi-client/gen/services/JournalsService.ts +++ b/gen/ts/webapi-client/gen/services/JournalsService.ts @@ -11,6 +11,7 @@ import type { JournalFindResult } from '../models/JournalFindResult'; import type { JournalLoadArgs } from '../models/JournalLoadArgs'; import type { JournalLoadForDateAndPeriodArgs } from '../models/JournalLoadForDateAndPeriodArgs'; import type { JournalLoadForDateAndPeriodResult } from '../models/JournalLoadForDateAndPeriodResult'; +import type { JournalLoadPublicArgs } from '../models/JournalLoadPublicArgs'; import type { JournalLoadResult } from '../models/JournalLoadResult'; import type { JournalLoadSettingsArgs } from '../models/JournalLoadSettingsArgs'; import type { JournalLoadSettingsResult } from '../models/JournalLoadSettingsResult'; @@ -190,6 +191,34 @@ export class JournalsService { }, }); } + /** + * Load a published journal and its dependent entities by publish external id. + * @param requestBody The input data + * @returns JournalLoadResult Successful response + * @throws ApiError + */ + public journalLoadPublic( + requestBody?: JournalLoadPublicArgs, + ): CancelablePromise<JournalLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/journal-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * The command for loading the settings around journals. * @param requestBody The input data diff --git a/itests/webui/entities/journals.test.py b/itests/webui/entities/journals.test.py index 82fe07c66..9f0d5a2d7 100644 --- a/itests/webui/entities/journals.test.py +++ b/itests/webui/entities/journals.test.py @@ -1,5 +1,7 @@ """Tests about journals.""" +import re + import pendulum import pytest from jupiter_webapi_client.api.journals.journal_create import ( @@ -19,7 +21,7 @@ ) from playwright.sync_api import Page, expect -from itests.helpers import get_parsed_from_response +from itests.helpers import get_parsed_from_response, open_leaf_publish_panel @pytest.fixture(autouse=True, scope="module") @@ -94,3 +96,36 @@ def test_webui_journal_view_all(page: Page, create_journal) -> None: expect(page.locator(f"#journal-{journal3.ref_id}").last).to_contain_text( f"Monthly journal for {pendulum.now().subtract(days=7).format('YYYY-MM-DD')}" ) + + +def test_webui_journal_publish_and_view_public(page: Page, create_journal) -> None: + journal = create_journal( + right_now="2024-07-01", + period=RecurringTaskPeriod.DAILY, + ) + page.goto(f"/app/workspace/journals/{journal.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Journal-publish") + page.locator("button[id='Journal-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/journals/{journal.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Journal-publish") + expect(page.locator("#Journal-publish")).to_contain_text("draft") + + page.locator("button[id='Journal-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/journals/{journal.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Journal-publish") + expect(page.locator("#Journal-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/journal/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="rightNow"]')).to_have_value("2024-07-01") diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index 1c1178463..5867e3192 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -36,7 +36,7 @@ NamedEntityTag.BIG_PLAN.value, NamedEntityTag.DOC.value, NamedEntityTag.DIR.value, - NamedEntityTag.JOURNAL.value, + NamedEntityTag.JOURNAL.value, # done NamedEntityTag.VACATION.value, # done NamedEntityTag.SMART_LIST.value, NamedEntityTag.SMART_LIST_ITEM.value, diff --git a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx index 32d8f3266..eff61dcac 100644 --- a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx +++ b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx @@ -381,25 +381,25 @@ export function LeafPanel(props: PropsWithChildren<LeafPanelProps>) { {(hasHistory || hasPublish) && ( <Box sx={{ marginLeft: "auto", display: "flex" }}> - {hasHistory && ( + {hasPublish && ( <IconButton + id="leaf-entity-publish" onClick={() => { - setShowPublish(false); - setShowHistory((h) => !h); + setShowHistory(false); + setShowPublish((p) => !p); }} > - <HistoryIcon color={showHistory ? "primary" : undefined} /> + <PublicIcon color={showPublish ? "primary" : undefined} /> </IconButton> )} - {hasPublish && ( + {hasHistory && ( <IconButton - id="leaf-entity-publish" onClick={() => { - setShowHistory(false); - setShowPublish((p) => !p); + setShowPublish(false); + setShowHistory((h) => !h); }} > - <PublicIcon color={showPublish ? "primary" : undefined} /> + <HistoryIcon color={showHistory ? "primary" : undefined} /> </IconButton> )} </Box> diff --git a/src/core/jupiter/core/journals/component/editor.tsx b/src/core/jupiter/core/journals/component/editor.tsx new file mode 100644 index 000000000..aaa212bce --- /dev/null +++ b/src/core/jupiter/core/journals/component/editor.tsx @@ -0,0 +1,109 @@ +import type { Journal, Tag } from "@jupiter/webapi-client"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; +import { entityLinkStd } from "#/core/common/entity-link"; +import { PeriodSelect } from "#/core/common/component/period-select"; +import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; +import type { ActionResult } from "#/core/infra/action-result"; +import { FieldError } from "#/core/infra/component/errors"; +import { + ActionSingle, + SectionActions, +} from "#/core/infra/component/section-actions"; +import { SectionCard } from "#/core/infra/component/section-card"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import type { TopLevelInfo } from "#/core/infra/top-level-context"; + +interface JournalEditorProps { + journal: Journal; + tags: Array<Tag>; + allTags: Array<Tag>; + inputsEnabled: boolean; + corePropertyEditable: boolean; + topLevelInfo: TopLevelInfo; + actionResult?: ActionResult<unknown>; +} + +export function JournalEditor(props: JournalEditorProps) { + const isBigScreen = useBigScreen(); + const { journal, tags, allTags } = props; + const timeConfigEditable = + props.inputsEnabled && props.corePropertyEditable; + + return ( + <SectionCard + title="Properties" + actions={ + props.inputsEnabled ? ( + <SectionActions + id="journal-properties" + topLevelInfo={props.topLevelInfo} + inputsEnabled={props.inputsEnabled} + actions={[ + ActionSingle({ + id: "journal-change-time-config", + text: "Change Time Config", + value: "change-time-config", + disabled: !timeConfigEditable, + highlight: true, + }), + ActionSingle({ + id: "journal-refresh-stats", + text: "Refresh Stats", + value: "refresh-stats", + disabled: !props.inputsEnabled, + }), + ]} + /> + ) : undefined + } + > + <Stack + direction={isBigScreen ? "row" : "column"} + spacing={2} + useFlexGap + > + <FormControl fullWidth> + <InputLabel id="rightNow" shrink margin="dense"> + The Date + </InputLabel> + <OutlinedInput + type="date" + notched + label="rightNow" + name="rightNow" + readOnly={!timeConfigEditable} + defaultValue={journal.right_now} + disabled={!timeConfigEditable} + /> + + <FieldError + actionResult={props.actionResult} + fieldName="/right_now" + /> + </FormControl> + + <FormControl fullWidth> + <PeriodSelect + labelId="period" + label="Period" + name="period" + inputsEnabled={timeConfigEditable} + defaultValue={journal.period} + /> + <FieldError actionResult={props.actionResult} fieldName="/status" /> + </FormControl> + + <FormControl fullWidth> + <TagsEditor + name="tags" + allTags={allTags} + defaultValue={tags.map((tag) => tag.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.JOURNAL, journal.ref_id)} + /> + </FormControl> + </Stack> + </SectionCard> + ); +} diff --git a/src/core/jupiter/core/journals/root.py b/src/core/jupiter/core/journals/root.py index d99443c3f..ffd26af4a 100644 --- a/src/core/jupiter/core/journals/root.py +++ b/src/core/jupiter/core/journals/root.py @@ -6,6 +6,7 @@ from jupiter.core.common.recurring_task_period import RecurringTaskPeriod from jupiter.core.common.sub.inbox_tasks.root import InboxTask from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.timeline import infer_timeline from jupiter.core.journals.source import JournalSource @@ -61,6 +62,9 @@ class Journal(LeafEntity): InboxTask, owner=IsEntityLinkStd(NamedEntityTag.JOURNAL.value), ) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.JOURNAL.value) + ) stats = ContainsOneRecord(JournalStats, journal_ref_id=IsRefId()) @staticmethod diff --git a/src/core/jupiter/core/journals/service/__init__.py b/src/core/jupiter/core/journals/service/__init__.py new file mode 100644 index 000000000..4c0b72f74 --- /dev/null +++ b/src/core/jupiter/core/journals/service/__init__.py @@ -0,0 +1 @@ +"""Services for journals.""" diff --git a/src/core/jupiter/core/journals/service/load.py b/src/core/jupiter/core/journals/service/load.py new file mode 100644 index 000000000..45d669d87 --- /dev/null +++ b/src/core/jupiter/core/journals/service/load.py @@ -0,0 +1,110 @@ +"""Shared service for loading a journal and its dependent entities.""" + +from jupiter.core.common import schedules +from jupiter.core.common.sub.inbox_tasks.root import InboxTask +from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.journals.root import Journal, JournalRepository +from jupiter.core.journals.stats import JournalStats, JournalStatsRepository +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result +from jupiter.framework.utils.generic_loader import generic_loader + + +@use_case_result +class JournalLoadResult(UseCaseResultBase): + """JournalLoadResult.""" + + journal: Journal + tags: list[Tag] + note: Note + journal_stats: JournalStats + writing_task: InboxTask | None + sub_period_journals: list[Journal] + publish_entity: PublishEntity | None + + +class JournalLoadService: + """Shared service for loading a journal and its dependent entities.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + journal: Journal, + *, + allow_archived: bool = False, + include_sub_period_journals: bool = True, + include_publish_entity: bool = True, + ) -> JournalLoadResult: + """Load a journal together with the entities that hang off it.""" + journal, note, writing_task = await generic_loader( + uow, + Journal, + journal.ref_id, + Journal.note, + Journal.writing_task, + allow_archived=allow_archived, + allow_subentity_archived=True, + ) + + journal_stats = await uow.get(JournalStatsRepository).load_by_key_optional( + journal.ref_id + ) + + if journal_stats is None: + raise Exception("Journal stats not found") + + if include_sub_period_journals: + schedule = schedules.get_schedule( + period=journal.period, + name=journal.name, + right_now=journal.right_now.to_timestamp_at_end_of_day(), + ) + + sub_period_journals = await uow.get(JournalRepository).find_all_in_range( + parent_ref_id=journal.journal_collection.ref_id, + allow_archived=False, + filter_periods=journal.period.all_smaller_periods, + filter_start_date=schedule.first_day, + filter_end_date=schedule.end_day, + ) + else: + sub_period_journals = [] + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std(NamedEntityTag.JOURNAL.value, journal.ref_id), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std(NamedEntityTag.JOURNAL.value, journal.ref_id), + allow_archived=allow_archived, + ) + + return JournalLoadResult( + journal=journal, + tags=tags, + note=note, + journal_stats=journal_stats, + writing_task=writing_task, + sub_period_journals=sub_period_journals, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/journals/use_case/load.py b/src/core/jupiter/core/journals/use_case/load.py index 210779cd3..d749cdb71 100644 --- a/src/core/jupiter/core/journals/use_case/load.py +++ b/src/core/jupiter/core/journals/use_case/load.py @@ -1,35 +1,24 @@ """Retrieve details about a journal.""" from jupiter.core.app import AppCore -from jupiter.core.common import schedules -from jupiter.core.common.sub.inbox_tasks.root import InboxTask -from jupiter.core.common.sub.notes.root import Note -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.journals.root import Journal, JournalRepository -from jupiter.core.journals.stats import ( - JournalStats, - JournalStatsRepository, -) -from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.journals.root import Journal +from jupiter.core.journals.service.load import JournalLoadResult, JournalLoadService from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import ( readonly_use_case, ) from jupiter.framework.use_case_io import ( UseCaseArgsBase, - UseCaseResultBase, use_case_args, - use_case_result, ) -from jupiter.framework.utils.generic_loader import generic_loader + +__all__ = ["JournalLoadArgs", "JournalLoadResult", "JournalLoadUseCase"] @use_case_args @@ -40,18 +29,6 @@ class JournalLoadArgs(UseCaseArgsBase): allow_archived: bool | None -@use_case_result -class JournalLoadResult(UseCaseResultBase): - """Result.""" - - journal: Journal - tags: list[Tag] - note: Note - journal_stats: JournalStats - writing_task: InboxTask | None - sub_period_journals: list[Journal] - - @readonly_use_case( WorkspaceFeature.JOURNALS, only_for_component=[AppCore.WEBUI, AppCore.API] ) @@ -69,54 +46,12 @@ async def _perform_transactional_read( """Execute the command's actions.""" allow_archived = args.allow_archived or False - journal, note, writing_task = await generic_loader( - uow, - Journal, - args.ref_id, - Journal.note, - Journal.writing_task, - allow_archived=allow_archived, - allow_subentity_archived=True, - ) - - journal_stats = await uow.get(JournalStatsRepository).load_by_key_optional( - journal.ref_id + journal = await uow.get_for(Journal).load_by_id( + args.ref_id, allow_archived=allow_archived ) - if journal_stats is None: - raise Exception("Journal stats not found") - - schedule = schedules.get_schedule( - period=journal.period, - name=journal.name, - right_now=journal.right_now.to_timestamp_at_end_of_day(), - ) - - sub_period_journals = await uow.get(JournalRepository).find_all_in_range( - parent_ref_id=journal.journal_collection.ref_id, - allow_archived=False, - filter_periods=journal.period.all_smaller_periods, - filter_start_date=schedule.first_day, - filter_end_date=schedule.end_day, - ) - - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.JOURNAL.value, journal.ref_id), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - - return JournalLoadResult( - journal=journal, - tags=tags, - note=note, - journal_stats=journal_stats, - writing_task=writing_task, - sub_period_journals=sub_period_journals, + return await JournalLoadService().do_it( + uow, + journal, + allow_archived=allow_archived, ) diff --git a/src/core/jupiter/core/journals/use_case/load_public.py b/src/core/jupiter/core/journals/use_case/load_public.py new file mode 100644 index 000000000..9a077c9eb --- /dev/null +++ b/src/core/jupiter/core/journals/use_case/load_public.py @@ -0,0 +1,80 @@ +"""Guest readonly use case for loading a published journal by external id.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.journals.collection import JournalCollection +from jupiter.core.journals.root import Journal +from jupiter.core.journals.service.load import JournalLoadResult, JournalLoadService +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import ( + UseCaseArgsBase, + use_case_args, +) + + +@use_case_args +class JournalLoadPublicArgs(UseCaseArgsBase): + """JournalLoadPublic args.""" + + external_id: PublishExternalId + + +class JournalLoadPublicUseCase( + JupiterGuestReadonlyUseCase[JournalLoadPublicArgs, JournalLoadResult] +): + """Load a published journal and its dependent entities by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: JournalLoadPublicArgs, + ) -> JournalLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.JOURNAL.value: + raise InputValidationError( + "The publish entity does not refer to a journal." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + journal_collection = await uow.get_for(JournalCollection).load_by_parent( + publish_domain.workspace.ref_id + ) + journal = await uow.get_for(Journal).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if journal.parent_ref_id != journal_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace journal." + ) + + return await JournalLoadService().do_it( + uow, + journal, + allow_archived=False, + include_sub_period_journals=False, + include_publish_entity=False, + ) diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx index 004cbdf3b..9140d750d 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -21,6 +21,8 @@ function publishedEntityLocation(externalId: string, owner: string): string { return `/app/public/published/todo-task/${externalId}`; case NamedEntityTag.VACATION: return `/app/public/published/vacation/${externalId}`; + case NamedEntityTag.JOURNAL: + return `/app/public/published/journal/${externalId}`; default: throw new Response(ReasonPhrases.NOT_FOUND, { status: StatusCodes.NOT_FOUND, diff --git a/src/webui/app/routes/app/public/published/journal/$externalId.tsx b/src/webui/app/routes/app/public/published/journal/$externalId.tsx new file mode 100644 index 000000000..c050a5c24 --- /dev/null +++ b/src/webui/app/routes/app/public/published/journal/$externalId.tsx @@ -0,0 +1,110 @@ +import { ApiError } from "@jupiter/webapi-client"; +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { JournalEditor } from "@jupiter/core/journals/component/editor"; +import { allowUserChanges } from "@jupiter/core/journals/source"; +import { ShowReport } from "@jupiter/core/report/component/show-report"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.journals.journalLoadPublic({ + external_id: externalId, + }); + + return json({ + journal: result.journal, + journalStats: result.journal_stats, + note: result.note, + tags: result.tags ?? [], + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedJournal() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const { journal, journalStats, note, tags } = loaderData; + + return ( + <LeafPanel + key={`published-journal-${journal.ref_id}`} + fakeKey={`published-journal-${journal.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <JournalEditor + journal={journal} + tags={tags} + allTags={tags} + inputsEnabled={false} + corePropertyEditable={allowUserChanges(journal.source)} + topLevelInfo={topLevelInfo} + /> + + <SectionCard title="Note"> + {note ? ( + <EntityNoteEditor initialNote={note} inputsEnabled={false} /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + + <SectionCard title="Report"> + <ShowReport + topLevelInfo={topLevelInfo} + allAspects={[]} + allGoals={[]} + report={journalStats.report} + /> + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => + `Could not find published journal ${params.externalId}!`, + error: (params) => + `There was an error loading published journal ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/workspace/journals/$id.tsx b/src/webui/app/routes/app/workspace/journals/$id.tsx index a5038d976..1bb7927f3 100644 --- a/src/webui/app/routes/app/workspace/journals/$id.tsx +++ b/src/webui/app/routes/app/workspace/journals/$id.tsx @@ -5,7 +5,6 @@ import { WorkspaceFeature, } from "@jupiter/webapi-client"; import type { GoalSummary, Tag } from "@jupiter/webapi-client"; -import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; @@ -20,24 +19,17 @@ import { isWorkspaceFeatureAvailable } from "@jupiter/core/workspaces/root"; import { sortTimePlansNaturally } from "@jupiter/core/time_plans/root"; import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; -import { FieldError, GlobalError } from "@jupiter/core/infra/component/errors"; +import { GlobalError } from "@jupiter/core/infra/component/errors"; import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; -import { - ActionSingle, - SectionActions, -} from "@jupiter/core/infra/component/section-actions"; import { SectionCard } from "@jupiter/core/infra/component/section-card"; import { JournalStack } from "@jupiter/core/journals/component/stack"; -import { PeriodSelect } from "@jupiter/core/common/component/period-select"; +import { JournalEditor } from "@jupiter/core/journals/component/editor"; import { ShowReport } from "@jupiter/core/report/component/show-report"; import { TimePlanStack } from "@jupiter/core/time_plans/component/stack"; import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; -import { useBigScreen } from "@jupiter/core/infra/component/use-big-screen"; -import { entityLinkStd } from "@jupiter/core/common/entity-link"; -import { TagsEditor } from "@jupiter/core/common/sub/tags/component/tags-editor"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; @@ -62,6 +54,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -112,6 +116,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { subTimePlans: timePlanResult?.sub_period_time_plans ?? [], tags: result.tags, allTags: allTags.tags as Array<Tag>, + publishEntity: result.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -168,6 +173,30 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/journals`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/journals/${id}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/journals/${id}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/journals/${id}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -195,8 +224,6 @@ export default function Journal() { const actionData = useActionData<typeof action>(); const navigation = useNavigation(); - const isBigScreen = useBigScreen(); - const topLevelInfo = useContext(TopLevelInfoContext); const corePropertyEditable = allowUserChanges(loaderData.journal.source); @@ -217,80 +244,19 @@ export default function Journal() { entityArchived={loaderData.journal.archived} returnLocation="/app/workspace/journals" initialExpansionState={LeafPanelExpansionState.FULL} + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <GlobalError actionResult={actionData} /> - <SectionCard - title="Properties" - actions={ - <SectionActions - id="journal-properties" - topLevelInfo={topLevelInfo} - inputsEnabled={inputsEnabled} - actions={[ - ActionSingle({ - id: "journal-change-time-config", - text: "Change Time Config", - value: "change-time-config", - disabled: !inputsEnabled || !corePropertyEditable, - highlight: true, - }), - ActionSingle({ - id: "journal-refresh-stats", - text: "Refresh Stats", - value: "refresh-stats", - disabled: !inputsEnabled, - }), - ]} - /> - } - > - <Stack - direction={isBigScreen ? "row" : "column"} - spacing={2} - useFlexGap - > - <FormControl fullWidth> - <InputLabel id="rightNow" shrink margin="dense"> - The Date - </InputLabel> - <OutlinedInput - type="date" - notched - label="rightNow" - name="rightNow" - readOnly={!inputsEnabled || !corePropertyEditable} - defaultValue={loaderData.journal.right_now} - disabled={!inputsEnabled || !corePropertyEditable} - /> - - <FieldError actionResult={actionData} fieldName="/right_now" /> - </FormControl> - - <FormControl fullWidth> - <PeriodSelect - labelId="period" - label="Period" - name="period" - inputsEnabled={inputsEnabled && corePropertyEditable} - defaultValue={loaderData.journal.period} - /> - <FieldError actionResult={actionData} fieldName="/status" /> - </FormControl> - - <FormControl fullWidth> - <TagsEditor - name="tags" - allTags={loaderData.allTags} - defaultValue={loaderData.tags.map((tag) => tag.ref_id)} - inputsEnabled={inputsEnabled} - owner={entityLinkStd( - NamedEntityTag.JOURNAL, - loaderData.journal.ref_id, - )} - /> - </FormControl> - </Stack> - </SectionCard> + <JournalEditor + journal={loaderData.journal} + tags={loaderData.tags} + allTags={loaderData.allTags} + inputsEnabled={inputsEnabled} + corePropertyEditable={corePropertyEditable} + topLevelInfo={topLevelInfo} + actionResult={actionData} + /> <SectionCard id="journal-note" title="Note"> <EntityNoteEditor From de5b69ed98237bc9effb9ea67027eb4dfa049c5b Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Sun, 7 Jun 2026 22:34:32 +0300 Subject: [PATCH 05/21] Add time plans now --- .../api/time_plans/time_plan_load_public.py | 212 ++++++++++ .../jupiter_webapi_client/models/__init__.py | 2 + .../models/time_plan_load_public_args.py | 62 +++ .../models/time_plan_load_result.py | 35 +- gen/ts/webapi-client/gen/index.ts | 1 + .../gen/models/TimePlanLoadPublicArgs.ts | 12 + .../gen/models/TimePlanLoadResult.ts | 4 +- .../gen/services/TimePlansService.ts | 29 ++ itests/helpers.py | 14 +- itests/webui/entities/time_plans.test.py | 31 ++ .../common/sub/publish/sub/entity/root.py | 2 +- .../infra/component/layout/branch-panel.tsx | 77 +++- .../core/time_plans/component/editor.tsx | 200 +++++++++ src/core/jupiter/core/time_plans/root.py | 4 + .../jupiter/core/time_plans/service/load.py | 379 +++++++++++++++++ .../jupiter/core/time_plans/use_case/load.py | 394 +----------------- .../core/time_plans/use_case/load_public.py | 84 ++++ .../app/public/published/$externalId.tsx | 2 + .../published/time-plan/$externalId.tsx | 186 +++++++++ .../routes/app/workspace/time-plans/$id.tsx | 213 +++------- 20 files changed, 1395 insertions(+), 548 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/time_plans/time_plan_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/time_plan_load_public_args.py create mode 100644 gen/ts/webapi-client/gen/models/TimePlanLoadPublicArgs.ts create mode 100644 src/core/jupiter/core/time_plans/component/editor.tsx create mode 100644 src/core/jupiter/core/time_plans/service/load.py create mode 100644 src/core/jupiter/core/time_plans/use_case/load_public.py create mode 100644 src/webui/app/routes/app/public/published/time-plan/$externalId.tsx diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/time_plans/time_plan_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/time_plans/time_plan_load_public.py new file mode 100644 index 000000000..1f0ad7170 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/time_plans/time_plan_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.time_plan_load_public_args import TimePlanLoadPublicArgs +from ...models.time_plan_load_result import TimePlanLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: TimePlanLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/time-plan-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | TimePlanLoadResult | None: + if response.status_code == 200: + response_200 = TimePlanLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | TimePlanLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: TimePlanLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | TimePlanLoadResult]: + """Load a published time plan and its dependent entities by publish external id. + + Args: + body (TimePlanLoadPublicArgs | Unset): TimePlanLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | TimePlanLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: TimePlanLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | TimePlanLoadResult | None: + """Load a published time plan and its dependent entities by publish external id. + + Args: + body (TimePlanLoadPublicArgs | Unset): TimePlanLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | TimePlanLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: TimePlanLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | TimePlanLoadResult]: + """Load a published time plan and its dependent entities by publish external id. + + Args: + body (TimePlanLoadPublicArgs | Unset): TimePlanLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | TimePlanLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: TimePlanLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | TimePlanLoadResult | None: + """Load a published time plan and its dependent entities by publish external id. + + Args: + body (TimePlanLoadPublicArgs | Unset): TimePlanLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | TimePlanLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index c174c4d23..f18a980ff 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -888,6 +888,7 @@ from .time_plan_load_args import TimePlanLoadArgs from .time_plan_load_for_date_and_period_args import TimePlanLoadForDateAndPeriodArgs from .time_plan_load_for_date_and_period_result import TimePlanLoadForDateAndPeriodResult +from .time_plan_load_public_args import TimePlanLoadPublicArgs from .time_plan_load_result import TimePlanLoadResult from .time_plan_load_result_activity_doneness_type_0 import TimePlanLoadResultActivityDonenessType0 from .time_plan_load_settings_args import TimePlanLoadSettingsArgs @@ -1879,6 +1880,7 @@ "TimePlanLoadArgs", "TimePlanLoadForDateAndPeriodArgs", "TimePlanLoadForDateAndPeriodResult", + "TimePlanLoadPublicArgs", "TimePlanLoadResult", "TimePlanLoadResultActivityDonenessType0", "TimePlanLoadSettingsArgs", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/time_plan_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/time_plan_load_public_args.py new file mode 100644 index 000000000..71fd0fa4c --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/time_plan_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="TimePlanLoadPublicArgs") + + +@_attrs_define +class TimePlanLoadPublicArgs: + """TimePlanLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + time_plan_load_public_args = cls( + external_id=external_id, + ) + + time_plan_load_public_args.additional_properties = d + return time_plan_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/time_plan_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/time_plan_load_result.py index 6be0455ba..5f24bef37 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/time_plan_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/time_plan_load_result.py @@ -15,6 +15,7 @@ from ..models.goal import Goal from ..models.inbox_task import InboxTask from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_plan import TimePlan from ..models.time_plan_activity import TimePlanActivity @@ -26,7 +27,7 @@ @_attrs_define class TimePlanLoadResult: - """Result. + """TimePlanLoadResult. Attributes: time_plan (TimePlan): A plan for a particular period of time. @@ -44,6 +45,7 @@ class TimePlanLoadResult: sub_period_time_plans (list[TimePlan] | None | Unset): higher_time_plan (None | TimePlan | Unset): previous_time_plan (None | TimePlan | Unset): + publish_entity (None | PublishEntity | Unset): """ time_plan: TimePlan @@ -61,9 +63,11 @@ class TimePlanLoadResult: sub_period_time_plans: list[TimePlan] | None | Unset = UNSET higher_time_plan: None | TimePlan | Unset = UNSET previous_time_plan: None | TimePlan | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.publish_entity import PublishEntity from ..models.time_plan import TimePlan from ..models.time_plan_load_result_activity_doneness_type_0 import TimePlanLoadResultActivityDonenessType0 @@ -180,6 +184,14 @@ def to_dict(self) -> dict[str, Any]: else: previous_time_plan = self.previous_time_plan + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -209,6 +221,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["higher_time_plan"] = higher_time_plan if previous_time_plan is not UNSET: field_dict["previous_time_plan"] = previous_time_plan + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -220,6 +234,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.goal import Goal from ..models.inbox_task import InboxTask from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_plan import TimePlan from ..models.time_plan_activity import TimePlanActivity @@ -434,6 +449,23 @@ def _parse_previous_time_plan(data: object) -> None | TimePlan | Unset: previous_time_plan = _parse_previous_time_plan(d.pop("previous_time_plan", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + time_plan_load_result = cls( time_plan=time_plan, tags=tags, @@ -450,6 +482,7 @@ def _parse_previous_time_plan(data: object) -> None | TimePlan | Unset: sub_period_time_plans=sub_period_time_plans, higher_time_plan=higher_time_plan, previous_time_plan=previous_time_plan, + publish_entity=publish_entity, ) time_plan_load_result.additional_properties = d diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index f227f8f15..30cbbb2f4 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -739,6 +739,7 @@ export type { TimePlanGoalLink } from './models/TimePlanGoalLink'; export type { TimePlanLoadArgs } from './models/TimePlanLoadArgs'; export type { TimePlanLoadForDateAndPeriodArgs } from './models/TimePlanLoadForDateAndPeriodArgs'; export type { TimePlanLoadForDateAndPeriodResult } from './models/TimePlanLoadForDateAndPeriodResult'; +export type { TimePlanLoadPublicArgs } from './models/TimePlanLoadPublicArgs'; export type { TimePlanLoadResult } from './models/TimePlanLoadResult'; export type { TimePlanLoadSettingsArgs } from './models/TimePlanLoadSettingsArgs'; export type { TimePlanLoadSettingsResult } from './models/TimePlanLoadSettingsResult'; diff --git a/gen/ts/webapi-client/gen/models/TimePlanLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/TimePlanLoadPublicArgs.ts new file mode 100644 index 000000000..3010941ef --- /dev/null +++ b/gen/ts/webapi-client/gen/models/TimePlanLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * TimePlanLoadPublic args. + */ +export type TimePlanLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/TimePlanLoadResult.ts b/gen/ts/webapi-client/gen/models/TimePlanLoadResult.ts index c77344b57..a3e7b0c23 100644 --- a/gen/ts/webapi-client/gen/models/TimePlanLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/TimePlanLoadResult.ts @@ -8,12 +8,13 @@ import type { Chapter } from './Chapter'; import type { Goal } from './Goal'; import type { InboxTask } from './InboxTask'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { Tag } from './Tag'; import type { TimePlan } from './TimePlan'; import type { TimePlanActivity } from './TimePlanActivity'; import type { TimePlanActivityDoneness } from './TimePlanActivityDoneness'; /** - * Result. + * TimePlanLoadResult. */ export type TimePlanLoadResult = { time_plan: TimePlan; @@ -31,5 +32,6 @@ export type TimePlanLoadResult = { sub_period_time_plans?: (Array<TimePlan> | null); higher_time_plan?: (TimePlan | null); previous_time_plan?: (TimePlan | null); + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/services/TimePlansService.ts b/gen/ts/webapi-client/gen/services/TimePlansService.ts index c17c5353b..231fb3a47 100644 --- a/gen/ts/webapi-client/gen/services/TimePlansService.ts +++ b/gen/ts/webapi-client/gen/services/TimePlansService.ts @@ -29,6 +29,7 @@ import type { TimePlanGenForTimePlanArgs } from '../models/TimePlanGenForTimePla import type { TimePlanLoadArgs } from '../models/TimePlanLoadArgs'; import type { TimePlanLoadForDateAndPeriodArgs } from '../models/TimePlanLoadForDateAndPeriodArgs'; import type { TimePlanLoadForDateAndPeriodResult } from '../models/TimePlanLoadForDateAndPeriodResult'; +import type { TimePlanLoadPublicArgs } from '../models/TimePlanLoadPublicArgs'; import type { TimePlanLoadResult } from '../models/TimePlanLoadResult'; import type { TimePlanLoadSettingsArgs } from '../models/TimePlanLoadSettingsArgs'; import type { TimePlanLoadSettingsResult } from '../models/TimePlanLoadSettingsResult'; @@ -515,6 +516,34 @@ export class TimePlansService { }, }); } + /** + * Load a published time plan and its dependent entities by publish external id. + * @param requestBody The input data + * @returns TimePlanLoadResult Successful response + * @throws ApiError + */ + public timePlanLoadPublic( + requestBody?: TimePlanLoadPublicArgs, + ): CancelablePromise<TimePlanLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/time-plan-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * The command for loading the settings around journals. * @param requestBody The input data diff --git a/itests/helpers.py b/itests/helpers.py index 01273ef8d..9abd95c35 100644 --- a/itests/helpers.py +++ b/itests/helpers.py @@ -53,11 +53,23 @@ def type_entity_note_editor_and_wait_for_save(page: Page, text: str) -> None: def open_leaf_publish_panel(page: Page, publish_section_id: str) -> None: """Open the publish panel in a leaf entity view.""" + open_entity_publish_panel(page, publish_section_id, "leaf-entity-publish") + + +def open_branch_publish_panel(page: Page, publish_section_id: str) -> None: + """Open the publish panel in a branch entity view.""" + open_entity_publish_panel(page, publish_section_id, "branch-entity-publish") + + +def open_entity_publish_panel( + page: Page, publish_section_id: str, publish_button_id: str +) -> None: + """Open the publish panel in a leaf or branch entity view.""" publish_panel = page.locator(f"#{publish_section_id}") if publish_panel.is_visible(): return - page.locator("button[id='leaf-entity-publish']").click() + page.locator(f"button[id='{publish_button_id}']").click() publish_panel.wait_for(state="visible") diff --git a/itests/webui/entities/time_plans.test.py b/itests/webui/entities/time_plans.test.py index 738842cf7..392f29edd 100644 --- a/itests/webui/entities/time_plans.test.py +++ b/itests/webui/entities/time_plans.test.py @@ -130,6 +130,7 @@ from itests.helpers import ( get_parsed_from_response, + open_branch_publish_panel, type_entity_note_editor_and_wait_for_save, ) @@ -319,6 +320,36 @@ def test_webui_time_plan_view_one(page: Page, create_time_plan) -> None: expect(page.locator('input[name="period"]')).to_have_value("daily") +def test_webui_time_plan_publish_and_view_public(page: Page, create_time_plan) -> None: + time_plan = create_time_plan("2024-06-18", RecurringTaskPeriod.DAILY) + page.goto(f"/app/workspace/time-plans/{time_plan.ref_id}") + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "TimePlan-publish") + page.locator("button[id='TimePlan-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/time-plans/{time_plan.ref_id}")) + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "TimePlan-publish") + expect(page.locator("#TimePlan-publish")).to_contain_text("draft") + + page.locator("button[id='TimePlan-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/time-plans/{time_plan.ref_id}")) + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "TimePlan-publish") + expect(page.locator("#TimePlan-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/time-plan/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="rightNow"]')).to_have_value("2024-06-18") + + def test_webui_time_plan_create(page: Page, create_time_plan) -> None: page.goto("/app/workspace/time-plans/new") page.wait_for_selector("#leaf-panel") diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index 5867e3192..6c58c2210 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -27,7 +27,7 @@ ALLOWED_PUBLISH_OWNER_TYPES: Final[frozenset[str]] = frozenset( { NamedEntityTag.TODO_TASK.value, # done - NamedEntityTag.TIME_PLAN.value, + NamedEntityTag.TIME_PLAN.value, # done NamedEntityTag.SCHEDULE_STREAM.value, NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, diff --git a/src/core/jupiter/core/infra/component/layout/branch-panel.tsx b/src/core/jupiter/core/infra/component/layout/branch-panel.tsx index fe42d1ede..a7cf662d3 100644 --- a/src/core/jupiter/core/infra/component/layout/branch-panel.tsx +++ b/src/core/jupiter/core/infra/component/layout/branch-panel.tsx @@ -6,6 +6,7 @@ import { Delete as DeleteIcon, DeleteForever as DeleteForeverIcon, History as HistoryIcon, + Public as PublicIcon, } from "@mui/icons-material"; import { Box, @@ -24,13 +25,20 @@ import { motion, useIsPresent } from "framer-motion"; import { type PropsWithChildren, useCallback, + useContext, useEffect, useRef, useState, } from "react"; -import type { EntityId, NamedEntityTag } from "@jupiter/webapi-client"; +import type { + EntityId, + NamedEntityTag, + PublishEntity, +} from "@jupiter/webapi-client"; +import { PublishPanel } from "#/core/common/sub/publish/components/publish-panel"; import { extractBranchFromPath } from "#/core/infra/routes"; +import { TopLevelInfoContext } from "#/core/infra/top-level-context"; import { restoreScrollPosition, saveScrollPosition, @@ -52,6 +60,8 @@ interface BranchPanelProps { entityArchived?: boolean; actions?: JSX.Element; returnLocation: string; + publishable?: boolean; + publishEntity?: PublishEntity; } export function BranchPanel(props: PropsWithChildren<BranchPanelProps>) { @@ -63,9 +73,15 @@ export function BranchPanel(props: PropsWithChildren<BranchPanelProps>) { const shouldShowALeaf = useTrunkNeedsToShowLeaf(); const [showArchiveDialog, setShowArchiveDialog] = useState(false); const [showHistory, setShowHistory] = useState(false); + const [showPublish, setShowPublish] = useState(false); + const topLevelInfo = useContext(TopLevelInfoContext); const hasHistory = props.entityType !== undefined && props.entityRefId !== undefined; + const hasPublish = + props.publishable === true && + props.entityType !== undefined && + props.entityRefId !== undefined; // This little function is a hack to get around the fact that Framer Motion // generates a translateX(Xpx) CSS applied to the StyledMotionDrawer element. @@ -198,20 +214,41 @@ export function BranchPanel(props: PropsWithChildren<BranchPanelProps>) { {props.actions} - {hasHistory && ( - <IconButton - sx={{ marginLeft: "auto" }} - onClick={() => setShowHistory((h) => !h)} - > - <HistoryIcon color={showHistory ? "primary" : undefined} /> - </IconButton> + {(hasHistory || hasPublish) && ( + <Box sx={{ marginLeft: "auto", display: "flex" }}> + {hasPublish && ( + <IconButton + id="branch-entity-publish" + onClick={() => { + setShowHistory(false); + setShowPublish((p) => !p); + }} + > + <PublicIcon color={showPublish ? "primary" : undefined} /> + </IconButton> + )} + {hasHistory && ( + <IconButton + onClick={() => { + setShowPublish(false); + setShowHistory((h) => !h); + }} + > + <HistoryIcon color={showHistory ? "primary" : undefined} /> + </IconButton> + )} + </Box> )} {props.showArchiveAndRemoveButton && ( <> <IconButton id="branch-entity-archive" - sx={!hasHistory ? { marginLeft: "auto" } : undefined} + sx={ + !hasHistory && !hasPublish + ? { marginLeft: "auto" } + : undefined + } disabled={!props.entityArchived && !props.inputsEnabled} type="button" onClick={() => setShowArchiveDialog(true)} @@ -257,7 +294,9 @@ export function BranchPanel(props: PropsWithChildren<BranchPanelProps>) { <IconButton sx={{ marginLeft: - !props.showArchiveAndRemoveButton && !hasHistory + !props.showArchiveAndRemoveButton && + !hasHistory && + !hasPublish ? "auto" : undefined, }} @@ -283,6 +322,24 @@ export function BranchPanel(props: PropsWithChildren<BranchPanelProps>) { entityRefId={props.entityRefId!} /> </BranchPanelContent> + ) : showPublish && hasPublish ? ( + <BranchPanelContent + id="branch-panel-content" + ref={containerRef} + isbigscreen={isBigScreen ? "true" : "false"} + hasleaf={shouldShowALeaf ? "true" : "false"} + > + <Stack spacing={2}> + <PublishPanel + entityType={props.entityType!} + entityRefId={props.entityRefId!} + topLevelInfo={topLevelInfo} + inputsEnabled={props.inputsEnabled ?? false} + publishEntity={props.publishEntity ?? null} + /> + </Stack> + <Box sx={{ height: "4rem" }}></Box> + </BranchPanelContent> ) : ( <BranchPanelContent id="branch-panel-content" diff --git a/src/core/jupiter/core/time_plans/component/editor.tsx b/src/core/jupiter/core/time_plans/component/editor.tsx new file mode 100644 index 000000000..671f5a5df --- /dev/null +++ b/src/core/jupiter/core/time_plans/component/editor.tsx @@ -0,0 +1,200 @@ +import type { + Aspect, + AspectSummary, + Chapter, + ChapterSummary, + Goal, + GoalSummary, + LifePlan, + MilestoneSummary, + Tag, + TimePlan, +} from "@jupiter/webapi-client"; +import { NamedEntityTag, WorkspaceFeature } from "@jupiter/webapi-client"; +import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; +import { aDateToDate } from "#/core/common/adate"; +import { entityLinkStd } from "#/core/common/entity-link"; +import { PeriodSelect } from "#/core/common/component/period-select"; +import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; +import { isWorkspaceFeatureAvailable } from "#/core/workspaces/root"; +import type { ActionResult } from "#/core/infra/action-result"; +import { FieldError } from "#/core/infra/component/errors"; +import { + ActionSingle, + SectionActions, +} from "#/core/infra/component/section-actions"; +import { SectionCard } from "#/core/infra/component/section-card"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import type { TopLevelInfo } from "#/core/infra/top-level-context"; +import { ChapterMultiSelect } from "#/core/life_plan/sub/chapters/components/multi-select"; +import { AspectMultiSelect } from "#/core/life_plan/sub/aspects/component/multi-select"; +import { lifePlanBirthdayDate } from "#/core/life_plan/root"; +import { GoalMultiSelect } from "#/core/life_plan/sub/goals/components/multi-select"; + +interface TimePlanEditorProps { + timePlan: TimePlan; + tags: Array<Tag>; + allTags: Array<Tag>; + aspects: Array<Aspect>; + chapters: Array<Chapter>; + goals: Array<Goal>; + lifePlan?: LifePlan; + allAspects?: Array<AspectSummary>; + allChapters?: Array<ChapterSummary>; + allGoals?: Array<GoalSummary>; + allMilestones?: Array<MilestoneSummary>; + inputsEnabled: boolean; + corePropertyEditable: boolean; + topLevelInfo: TopLevelInfo; + actionResult?: ActionResult<unknown>; +} + +export function TimePlanEditor(props: TimePlanEditorProps) { + const isBigScreen = useBigScreen(); + const { timePlan, tags, allTags, aspects, chapters, goals } = props; + const timeConfigEditable = + props.inputsEnabled && props.corePropertyEditable; + const changeTimeConfigIntent = props.corePropertyEditable + ? "change-time-config" + : "change-time-config-for-generated"; + + return ( + <SectionCard + title="Properties" + actions={ + props.inputsEnabled ? ( + <SectionActions + id="time-plan-properties" + topLevelInfo={props.topLevelInfo} + inputsEnabled={props.inputsEnabled} + actions={[ + ActionSingle({ + id: "time-plan-change-time-config", + text: "Change Time Config", + value: changeTimeConfigIntent, + disabled: !props.inputsEnabled, + highlight: true, + }), + ]} + /> + ) : undefined + } + > + <Stack + direction={isBigScreen ? "row" : "column"} + spacing={2} + useFlexGap + > + <FormControl fullWidth={!isBigScreen}> + <InputLabel id="rightNow" shrink margin="dense"> + The Date + </InputLabel> + <OutlinedInput + type="date" + notched + label="rightNow" + name="rightNow" + readOnly={!timeConfigEditable} + disabled={!timeConfigEditable} + defaultValue={timePlan.right_now} + /> + + <FieldError actionResult={props.actionResult} fieldName="/rightNow" /> + </FormControl> + + <FormControl fullWidth={!isBigScreen}> + <PeriodSelect + labelId="period" + label="Period" + name="period" + inputsEnabled={timeConfigEditable} + defaultValue={timePlan.period} + compact + /> + <FieldError actionResult={props.actionResult} fieldName="/period" /> + <FieldError actionResult={props.actionResult} fieldName="/status" /> + </FormControl> + + <FormControl fullWidth={!isBigScreen}> + <TagsEditor + name="tags_names" + allTags={allTags} + defaultValue={tags.map((t) => t.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.TIME_PLAN, timePlan.ref_id)} + aloneOnLine={!isBigScreen} + /> + </FormControl> + + {props.lifePlan && + isWorkspaceFeatureAvailable( + props.topLevelInfo.workspace, + WorkspaceFeature.LIFE_PLAN, + ) && ( + <> + <FormControl + fullWidth={!isBigScreen} + sx={{ width: isBigScreen ? "15%" : "100%" }} + > + <AspectMultiSelect + name="aspectRefIds" + label="Aspect" + inputsEnabled={props.inputsEnabled} + disabled={false} + allAspects={props.allAspects ?? aspects} + maxSelections={props.lifePlan.time_plan_max_life_plan_links} + defaultValue={aspects.map((p) => p.ref_id)} + /> + <FieldError + actionResult={props.actionResult} + fieldName="/aspectRefIds" + /> + </FormControl> + + <FormControl + fullWidth={!isBigScreen} + sx={{ width: isBigScreen ? "15%" : "100%" }} + > + <ChapterMultiSelect + name="chapterRefIds" + label="Chapter" + inputsEnabled={props.inputsEnabled} + disabled={false} + allChapters={props.allChapters ?? chapters} + maxSelections={props.lifePlan.time_plan_max_life_plan_links} + defaultValue={chapters.map((c) => c.ref_id)} + birthday={lifePlanBirthdayDate(props.lifePlan)} + today={aDateToDate(props.topLevelInfo.today)} + allMilestones={props.allMilestones ?? []} + allAspects={props.allAspects ?? aspects} + /> + <FieldError + actionResult={props.actionResult} + fieldName="/chapterRefIds" + /> + </FormControl> + + <FormControl + fullWidth={!isBigScreen} + sx={{ width: isBigScreen ? "15%" : "100%" }} + > + <GoalMultiSelect + name="goalRefIds" + label="Goal" + inputsEnabled={props.inputsEnabled} + disabled={false} + allGoals={props.allGoals ?? goals} + maxSelections={props.lifePlan.time_plan_max_life_plan_links} + defaultValue={goals.map((g) => g.ref_id)} + /> + <FieldError + actionResult={props.actionResult} + fieldName="/goalRefIds" + /> + </FormControl> + </> + )} + </Stack> + </SectionCard> + ); +} diff --git a/src/core/jupiter/core/time_plans/root.py b/src/core/jupiter/core/time_plans/root.py index 2f56ea39e..b1c03270e 100644 --- a/src/core/jupiter/core/time_plans/root.py +++ b/src/core/jupiter/core/time_plans/root.py @@ -6,6 +6,7 @@ from jupiter.core.common.recurring_task_period import RecurringTaskPeriod from jupiter.core.common.sub.inbox_tasks.root import InboxTask from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.timeline import infer_timeline from jupiter.core.named_entity_tag import NamedEntityTag @@ -79,6 +80,9 @@ class TimePlan(LeafEntity): InboxTask, owner=IsEntityLinkStd(NamedEntityTag.TIME_PLAN.value), ) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.TIME_PLAN.value) + ) @staticmethod @create_entity_action diff --git a/src/core/jupiter/core/time_plans/service/load.py b/src/core/jupiter/core/time_plans/service/load.py new file mode 100644 index 000000000..861a7d332 --- /dev/null +++ b/src/core/jupiter/core/time_plans/service/load.py @@ -0,0 +1,379 @@ +"""Shared service for loading a time plan and its dependent entities.""" + +from collections import defaultdict +from typing import cast + +from jupiter.core.big_plans.collection import BigPlanCollection +from jupiter.core.big_plans.root import BigPlan, BigPlanRepository +from jupiter.core.common import schedules +from jupiter.core.common.sub.inbox_tasks import parent_link_namespace +from jupiter.core.common.sub.inbox_tasks.collection import InboxTaskCollection +from jupiter.core.common.sub.inbox_tasks.root import InboxTask, InboxTaskRepository +from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.features import WorkspaceFeature +from jupiter.core.life_plan.root import LifePlan +from jupiter.core.life_plan.sub.aspects.root import Aspect +from jupiter.core.life_plan.sub.chapters.root import Chapter +from jupiter.core.life_plan.sub.goals.root import Goal +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.time_plans.life_plan_links import ( + TimePlanAspectLink, + TimePlanChapterLink, + TimePlanGoalLink, +) +from jupiter.core.time_plans.root import TimePlan, TimePlanRepository +from jupiter.core.time_plans.sub.activity.doneness import TimePlanActivityDoneness +from jupiter.core.time_plans.sub.activity.kind import TimePlanActivityKind +from jupiter.core.time_plans.sub.activity.root import TimePlanActivity +from jupiter.core.workspaces.root import Workspace +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result +from jupiter.framework.utils.generic_loader import generic_loader + + +@use_case_result +class TimePlanLoadResult(UseCaseResultBase): + """TimePlanLoadResult.""" + + time_plan: TimePlan + tags: list[Tag] + note: Note + activities: list[TimePlanActivity] + chapters: list[Chapter] + aspects: list[Aspect] + goals: list[Goal] + target_inbox_tasks: list[InboxTask] | None + target_big_plans: list[BigPlan] | None + activity_doneness: dict[EntityId, TimePlanActivityDoneness] | None + completed_nontarget_inbox_tasks: list[InboxTask] | None + completed_nottarget_big_plans: list[BigPlan] | None + sub_period_time_plans: list[TimePlan] | None + higher_time_plan: TimePlan | None + previous_time_plan: TimePlan | None + publish_entity: PublishEntity | None + + +class TimePlanLoadService: + """Shared service for loading a time plan and its dependent entities.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace: Workspace, + time_plan: TimePlan, + *, + allow_archived: bool = False, + include_targets: bool = False, + include_completed_nontarget: bool = False, + include_other_time_plans: bool = False, + include_publish_entity: bool = True, + ) -> TimePlanLoadResult: + """Load a time plan together with the entities that hang off it.""" + time_plan, activities, note = await generic_loader( + uow, + TimePlan, + time_plan.ref_id, + TimePlan.activities, + TimePlan.note, + allow_archived=allow_archived, + allow_subentity_archived=False, + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std(NamedEntityTag.TIME_PLAN.value, time_plan.ref_id), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + schedule = schedules.get_schedule( + period=time_plan.period, + name=time_plan.name, + right_now=time_plan.right_now.to_timestamp_at_end_of_day(), + ) + + chapters: list[Chapter] = [] + aspects: list[Aspect] = [] + goals: list[Goal] = [] + if workspace.is_feature_available(WorkspaceFeature.LIFE_PLAN): + life_plan = await uow.get_for(LifePlan).load_by_parent(workspace.ref_id) + chapter_links = await uow.get_for_record(TimePlanChapterLink).find_all( + time_plan.ref_id + ) + aspect_links = await uow.get_for_record(TimePlanAspectLink).find_all( + time_plan.ref_id + ) + goal_links = await uow.get_for_record(TimePlanGoalLink).find_all( + time_plan.ref_id + ) + + chapter_ref_ids = list({link.chapter_ref_id for link in chapter_links}) + aspect_ref_ids = list({link.aspect_ref_id for link in aspect_links}) + goal_ref_ids = list({link.goal_ref_id for link in goal_links}) + + if chapter_ref_ids: + chapters = await uow.get_for(Chapter).find_all( + parent_ref_id=life_plan.ref_id, + allow_archived=True, + filter_ref_ids=chapter_ref_ids, + ) + if aspect_ref_ids: + aspects = await uow.get_for(Aspect).find_all( + parent_ref_id=life_plan.ref_id, + allow_archived=True, + filter_ref_ids=aspect_ref_ids, + ) + if goal_ref_ids: + goals = await uow.get_for(Goal).find_all( + parent_ref_id=life_plan.ref_id, + allow_archived=True, + filter_ref_ids=goal_ref_ids, + ) + + target_inbox_tasks = None + inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( + workspace.ref_id + ) + if include_targets: + target_inbox_tasks = await uow.get_for(InboxTask).find_all( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=True, + filter_ref_ids=[ + a.target.ref_id for a in activities if a.is_target_inbox_task + ], + ) + + completed_nontarget_inbox_tasks = None + if include_completed_nontarget and target_inbox_tasks is not None: + completed_nontarget_inbox_tasks = await uow.get( + InboxTaskRepository + ).find_completed_in_range( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=True, + filter_start_completed_date=schedule.first_day, + filter_end_completed_date=schedule.end_day, + filter_include_parent_link_namespaces=[ + parent_link_namespace.TODO_TASK, + parent_link_namespace.BIG_PLAN, + ], + filter_exclude_ref_ids=[it.ref_id for it in target_inbox_tasks], + ) + + target_big_plans = None + completed_nontarget_big_plans = None + if workspace.is_feature_available(WorkspaceFeature.BIG_PLANS): + big_plan_collection = await uow.get_for(BigPlanCollection).load_by_parent( + workspace.ref_id + ) + + if include_targets: + target_big_plans = await uow.get_for(BigPlan).find_all( + parent_ref_id=big_plan_collection.ref_id, + allow_archived=True, + filter_ref_ids=[ + a.target.ref_id for a in activities if a.is_target_big_plan + ], + ) + + if include_completed_nontarget and target_big_plans is not None: + completed_nontarget_big_plans = await uow.get( + BigPlanRepository + ).find_completed_in_range( + parent_ref_id=big_plan_collection.ref_id, + allow_archived=True, + filter_start_completed_date=schedule.first_day, + filter_end_completed_date=schedule.end_day, + filter_exclude_ref_ids=[bp.ref_id for bp in target_big_plans], + ) + + activity_doneness = None + if include_targets: + activity_doneness = {} + target_inbox_tasks_by_ref_id = { + it.ref_id: it for it in cast(list[InboxTask], target_inbox_tasks) + } + target_big_plans_by_ref_id = ( + {bp.ref_id: bp for bp in target_big_plans} if target_big_plans else {} + ) + activities_by_big_plan_ref_id: defaultdict[EntityId, list[EntityId]] = ( + defaultdict(list) + ) + + for activity in activities: + if not activity.is_target_inbox_task: + continue + + inbox_task = target_inbox_tasks_by_ref_id[activity.target.ref_id] + + if activity.kind == TimePlanActivityKind.FINISH: + if inbox_task.is_completed: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.DONE + ) + elif inbox_task.is_working: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.WORKING + ) + else: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.NOT_DONE + ) + elif activity.kind == TimePlanActivityKind.MAKE_PROGRESS: + modified_in_time_plan = ( + inbox_task.is_working_or_more + and time_plan.start_date.to_timestamp_at_start_of_day() + <= inbox_task.last_modified_time + and inbox_task.last_modified_time + <= time_plan.end_date.add_days(30).to_timestamp_at_end_of_day() + ) + if inbox_task.is_completed or modified_in_time_plan: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.DONE + ) + elif inbox_task.is_working: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.WORKING + ) + else: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.NOT_DONE + ) + + if inbox_task.owner.the_type == NamedEntityTag.BIG_PLAN.value: + activities_by_big_plan_ref_id[inbox_task.owner.ref_id].append( + activity.ref_id + ) + + for activity in activities: + if not activity.is_target_big_plan: + continue + + if activity.target.ref_id not in target_big_plans_by_ref_id: + activity_doneness[activity.ref_id] = TimePlanActivityDoneness.DONE + continue + + big_plan = target_big_plans_by_ref_id[activity.target.ref_id] + + some_subactivity_is_working_or_done = ( + any( + activity_doneness[a] + in ( + TimePlanActivityDoneness.WORKING, + TimePlanActivityDoneness.DONE, + ) + for a in activities_by_big_plan_ref_id[big_plan.ref_id] + ) + if len(activities_by_big_plan_ref_id[big_plan.ref_id]) > 0 + else False + ) + + all_subactivities_are_done = ( + all( + activity_doneness[a] == TimePlanActivityDoneness.DONE + for a in activities_by_big_plan_ref_id[big_plan.ref_id] + ) + if len(activities_by_big_plan_ref_id[big_plan.ref_id]) > 0 + else False + ) + + if activity.kind == TimePlanActivityKind.FINISH: + if big_plan.is_completed: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.DONE + ) + elif some_subactivity_is_working_or_done: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.WORKING + ) + else: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.NOT_DONE + ) + elif activity.kind == TimePlanActivityKind.MAKE_PROGRESS: + modified_in_time_plan = ( + big_plan.is_working_or_more + and time_plan.start_date.to_timestamp_at_start_of_day() + <= big_plan.last_modified_time + and big_plan.last_modified_time + <= time_plan.end_date.add_days(60).to_timestamp_at_end_of_day() + ) + + if big_plan.is_completed or all_subactivities_are_done: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.DONE + ) + elif modified_in_time_plan or some_subactivity_is_working_or_done: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.WORKING + ) + else: + activity_doneness[activity.ref_id] = ( + TimePlanActivityDoneness.NOT_DONE + ) + + sub_period_time_plans = None + higher_time_plan = None + previous_time_plan = None + if include_other_time_plans: + sub_period_time_plans = await uow.get(TimePlanRepository).find_all_in_range( + parent_ref_id=time_plan.time_plan_domain.ref_id, + allow_archived=False, + filter_periods=time_plan.period.all_smaller_periods, + filter_start_date=schedule.first_day, + filter_end_date=schedule.end_day, + ) + + higher_time_plan = await uow.get(TimePlanRepository).find_higher( + parent_ref_id=time_plan.time_plan_domain.ref_id, + allow_archived=False, + period=time_plan.period, + right_now=time_plan.right_now, + ) + + previous_time_plan = await uow.get(TimePlanRepository).find_previous( + parent_ref_id=time_plan.time_plan_domain.ref_id, + allow_archived=False, + period=time_plan.period, + right_now=time_plan.right_now, + ) + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std(NamedEntityTag.TIME_PLAN.value, time_plan.ref_id), + allow_archived=allow_archived, + ) + + return TimePlanLoadResult( + time_plan=time_plan, + tags=tags, + note=note, + activities=list(activities), + chapters=chapters, + aspects=aspects, + goals=goals, + target_inbox_tasks=target_inbox_tasks, + target_big_plans=target_big_plans, + activity_doneness=activity_doneness, + completed_nontarget_inbox_tasks=completed_nontarget_inbox_tasks, + completed_nottarget_big_plans=completed_nontarget_big_plans, + sub_period_time_plans=sub_period_time_plans, + higher_time_plan=higher_time_plan, + previous_time_plan=previous_time_plan, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/time_plans/use_case/load.py b/src/core/jupiter/core/time_plans/use_case/load.py index 2faf5a09e..267085b4c 100644 --- a/src/core/jupiter/core/time_plans/use_case/load.py +++ b/src/core/jupiter/core/time_plans/use_case/load.py @@ -1,62 +1,19 @@ """Retrieve details about a time plan.""" -from collections import defaultdict -from typing import cast - from jupiter.core.app import AppCore -from jupiter.core.big_plans.collection import BigPlanCollection -from jupiter.core.big_plans.root import BigPlan, BigPlanRepository -from jupiter.core.common import schedules -from jupiter.core.common.sub.inbox_tasks import parent_link_namespace -from jupiter.core.common.sub.inbox_tasks.collection import ( - InboxTaskCollection, -) -from jupiter.core.common.sub.inbox_tasks.root import ( - InboxTask, - InboxTaskRepository, -) -from jupiter.core.common.sub.notes.root import Note -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.life_plan.root import LifePlan -from jupiter.core.life_plan.sub.aspects.root import Aspect -from jupiter.core.life_plan.sub.chapters.root import Chapter -from jupiter.core.life_plan.sub.goals.root import Goal -from jupiter.core.named_entity_tag import NamedEntityTag -from jupiter.core.time_plans.life_plan_links import ( - TimePlanAspectLink, - TimePlanChapterLink, - TimePlanGoalLink, -) -from jupiter.core.time_plans.root import ( - TimePlan, - TimePlanRepository, -) -from jupiter.core.time_plans.sub.activity.doneness import ( - TimePlanActivityDoneness, -) -from jupiter.core.time_plans.sub.activity.kind import ( - TimePlanActivityKind, -) -from jupiter.core.time_plans.sub.activity.root import TimePlanActivity +from jupiter.core.time_plans.root import TimePlan +from jupiter.core.time_plans.service.load import TimePlanLoadResult, TimePlanLoadService from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.storage.repository import DomainUnitOfWork -from jupiter.framework.use_case import ( - readonly_use_case, -) -from jupiter.framework.use_case_io import ( - UseCaseArgsBase, - UseCaseResultBase, - use_case_args, - use_case_result, -) -from jupiter.framework.utils.generic_loader import generic_loader +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + +__all__ = ["TimePlanLoadArgs", "TimePlanLoadResult", "TimePlanLoadUseCase"] @use_case_args @@ -70,27 +27,6 @@ class TimePlanLoadArgs(UseCaseArgsBase): include_other_time_plans: bool | None -@use_case_result -class TimePlanLoadResult(UseCaseResultBase): - """Result.""" - - time_plan: TimePlan - tags: list[Tag] - note: Note - activities: list[TimePlanActivity] - chapters: list[Chapter] - aspects: list[Aspect] - goals: list[Goal] - target_inbox_tasks: list[InboxTask] | None - target_big_plans: list[BigPlan] | None - activity_doneness: dict[EntityId, TimePlanActivityDoneness] | None - completed_nontarget_inbox_tasks: list[InboxTask] | None - completed_nottarget_big_plans: list[BigPlan] | None - sub_period_time_plans: list[TimePlan] | None - higher_time_plan: TimePlan | None - previous_time_plan: TimePlan | None - - @readonly_use_case( WorkspaceFeature.TIME_PLANS, only_for_component=[AppCore.WEBUI, AppCore.API] ) @@ -111,316 +47,16 @@ async def _perform_transactional_read( include_completed_nontarget = args.include_completed_nontarget or False include_other_time_plans = args.include_other_time_plans or False - workspace = context.workspace + time_plan = await uow.get_for(TimePlan).load_by_id( + args.ref_id, allow_archived=allow_archived + ) - time_plan, activities, note = await generic_loader( + return await TimePlanLoadService().do_it( uow, - TimePlan, - args.ref_id, - TimePlan.activities, - TimePlan.note, + context.workspace, + time_plan, allow_archived=allow_archived, - allow_subentity_archived=False, - ) - - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.TIME_PLAN.value, time_plan.ref_id), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - - schedule = schedules.get_schedule( - period=time_plan.period, - name=time_plan.name, - right_now=time_plan.right_now.to_timestamp_at_end_of_day(), - ) - - chapters: list[Chapter] = [] - aspects: list[Aspect] = [] - goals: list[Goal] = [] - if workspace.is_feature_available(WorkspaceFeature.LIFE_PLAN): - life_plan = await uow.get_for(LifePlan).load_by_parent(workspace.ref_id) - chapter_links = await uow.get_for_record(TimePlanChapterLink).find_all( - time_plan.ref_id - ) - aspect_links = await uow.get_for_record(TimePlanAspectLink).find_all( - time_plan.ref_id - ) - goal_links = await uow.get_for_record(TimePlanGoalLink).find_all( - time_plan.ref_id - ) - - chapter_ref_ids = list({link.chapter_ref_id for link in chapter_links}) - aspect_ref_ids = list({link.aspect_ref_id for link in aspect_links}) - goal_ref_ids = list({link.goal_ref_id for link in goal_links}) - - if chapter_ref_ids: - chapters = await uow.get_for(Chapter).find_all( - parent_ref_id=life_plan.ref_id, - allow_archived=True, - filter_ref_ids=chapter_ref_ids, - ) - if aspect_ref_ids: - aspects = await uow.get_for(Aspect).find_all( - parent_ref_id=life_plan.ref_id, - allow_archived=True, - filter_ref_ids=aspect_ref_ids, - ) - if goal_ref_ids: - goals = await uow.get_for(Goal).find_all( - parent_ref_id=life_plan.ref_id, - allow_archived=True, - filter_ref_ids=goal_ref_ids, - ) - - target_inbox_tasks = None - inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( - workspace.ref_id - ) - if include_targets: - target_inbox_tasks = await uow.get_for(InboxTask).find_all( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=True, - filter_ref_ids=[ - a.target.ref_id for a in activities if a.is_target_inbox_task - ], - ) - - completed_nontarget_inbox_tasks = None - if include_completed_nontarget and target_inbox_tasks is not None: - - # The rule here should be: - # If this is a inbox task or big plan include it always - # If this is a generated one, then: - # If the recurring_task_period is strictly higher than the time plan is we include it - # If the recurring_task_period is equal or lower than the time plan one we skip it - # expressed as: (it.source in (user, big-plan)) or (it.period in (*all_higher_periods) - # But this is hard to express cause inbox_tasks don't yet remember the period - # of their source entity. Inference from the timeline is hard in SQL, etc. - completed_nontarget_inbox_tasks = await uow.get( - InboxTaskRepository - ).find_completed_in_range( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=True, - filter_start_completed_date=schedule.first_day, - filter_end_completed_date=schedule.end_day, - filter_include_parent_link_namespaces=[ - parent_link_namespace.TODO_TASK, - parent_link_namespace.BIG_PLAN, - ], - filter_exclude_ref_ids=[it.ref_id for it in target_inbox_tasks], - ) - - target_big_plans = None - completed_nontarget_big_plans = None - if workspace.is_feature_available(WorkspaceFeature.BIG_PLANS): - big_plan_collection = await uow.get_for(BigPlanCollection).load_by_parent( - workspace.ref_id - ) - - if include_targets: - target_big_plans = await uow.get_for(BigPlan).find_all( - parent_ref_id=big_plan_collection.ref_id, - allow_archived=True, - filter_ref_ids=[ - a.target.ref_id for a in activities if a.is_target_big_plan - ], - ) - - if include_completed_nontarget and target_big_plans is not None: - completed_nontarget_big_plans = await uow.get( - BigPlanRepository - ).find_completed_in_range( - parent_ref_id=big_plan_collection.ref_id, - allow_archived=True, - filter_start_completed_date=schedule.first_day, - filter_end_completed_date=schedule.end_day, - filter_exclude_ref_ids=[bp.ref_id for bp in target_big_plans], - ) - - activity_doneness = None - if include_targets: - activity_doneness = {} - target_inbox_tasks_by_ref_id = { - it.ref_id: it for it in cast(list[InboxTask], target_inbox_tasks) - } - target_big_plans_by_ref_id = ( - {bp.ref_id: bp for bp in target_big_plans} if target_big_plans else {} - ) - activities_by_big_plan_ref_id: defaultdict[EntityId, list[EntityId]] = ( - defaultdict(list) - ) - - for activity in activities: - if not activity.is_target_inbox_task: - continue - - inbox_task = target_inbox_tasks_by_ref_id[activity.target.ref_id] - - if activity.kind == TimePlanActivityKind.FINISH: - if inbox_task.is_completed: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.DONE - ) - elif inbox_task.is_working: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.WORKING - ) - else: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.NOT_DONE - ) - elif activity.kind == TimePlanActivityKind.MAKE_PROGRESS: - # A tricky business logic decision. - # It's quite often the case that we setup a time plan with an inbox - # task where we wish to make progress. But the progress we do is just - # a bit after the time plan is created. So we'd still wish to show - # this as "done" in whatever view we have. So we add a buffer of a - # month afterwards to capture this. - modified_in_time_plan = ( - inbox_task.is_working_or_more - and time_plan.start_date.to_timestamp_at_start_of_day() - <= inbox_task.last_modified_time - and inbox_task.last_modified_time - <= time_plan.end_date.add_days(30).to_timestamp_at_end_of_day() - ) - if inbox_task.is_completed or modified_in_time_plan: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.DONE - ) - elif inbox_task.is_working: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.WORKING - ) - else: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.NOT_DONE - ) - - if inbox_task.owner.the_type == NamedEntityTag.BIG_PLAN.value: - activities_by_big_plan_ref_id[inbox_task.owner.ref_id].append( - activity.ref_id - ) - - for activity in activities: - if not activity.is_target_big_plan: - continue - - if activity.target.ref_id not in target_big_plans_by_ref_id: - activity_doneness[activity.ref_id] = TimePlanActivityDoneness.DONE - continue - - big_plan = target_big_plans_by_ref_id[activity.target.ref_id] - - some_subactivity_is_working_or_done = ( - any( - activity_doneness[a] - in ( - TimePlanActivityDoneness.WORKING, - TimePlanActivityDoneness.DONE, - ) - for a in activities_by_big_plan_ref_id[big_plan.ref_id] - ) - if len(activities_by_big_plan_ref_id[big_plan.ref_id]) > 0 - else False - ) - - all_subactivities_are_done = ( - all( - activity_doneness[a] == TimePlanActivityDoneness.DONE - for a in activities_by_big_plan_ref_id[big_plan.ref_id] - ) - if len(activities_by_big_plan_ref_id[big_plan.ref_id]) > 0 - else False - ) - - if activity.kind == TimePlanActivityKind.FINISH: - if big_plan.is_completed: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.DONE - ) - elif some_subactivity_is_working_or_done: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.WORKING - ) - else: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.NOT_DONE - ) - elif activity.kind == TimePlanActivityKind.MAKE_PROGRESS: - # A tricky business logic decision. - # It's quite often the case that we setup a time plan with an inbox - # task where we wish to make progress. But the progress we do is just - # a bit after the time plan is created. So we'd still wish to show - # this as "done" in whatever view we have. So we add a buffer of a - # month afterwards to capture this. - modified_in_time_plan = ( - big_plan.is_working_or_more - and time_plan.start_date.to_timestamp_at_start_of_day() - <= big_plan.last_modified_time - and big_plan.last_modified_time - <= time_plan.end_date.add_days(60).to_timestamp_at_end_of_day() - ) - - if big_plan.is_completed or all_subactivities_are_done: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.DONE - ) - elif modified_in_time_plan or some_subactivity_is_working_or_done: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.WORKING - ) - else: - activity_doneness[activity.ref_id] = ( - TimePlanActivityDoneness.NOT_DONE - ) - - sub_period_time_plans = None - higher_time_plan = None - previous_time_plan = None - if include_other_time_plans: - sub_period_time_plans = await uow.get(TimePlanRepository).find_all_in_range( - parent_ref_id=time_plan.time_plan_domain.ref_id, - allow_archived=False, - filter_periods=time_plan.period.all_smaller_periods, - filter_start_date=schedule.first_day, - filter_end_date=schedule.end_day, - ) - - higher_time_plan = await uow.get(TimePlanRepository).find_higher( - parent_ref_id=time_plan.time_plan_domain.ref_id, - allow_archived=False, - period=time_plan.period, - right_now=time_plan.right_now, - ) - - previous_time_plan = await uow.get(TimePlanRepository).find_previous( - parent_ref_id=time_plan.time_plan_domain.ref_id, - allow_archived=False, - period=time_plan.period, - right_now=time_plan.right_now, - ) - - return TimePlanLoadResult( - time_plan=time_plan, - tags=tags, - note=note, - activities=list(activities), - chapters=chapters, - aspects=aspects, - goals=goals, - target_inbox_tasks=target_inbox_tasks, - target_big_plans=target_big_plans, - activity_doneness=activity_doneness, - completed_nontarget_inbox_tasks=completed_nontarget_inbox_tasks, - completed_nottarget_big_plans=completed_nontarget_big_plans, - sub_period_time_plans=sub_period_time_plans, - higher_time_plan=higher_time_plan, - previous_time_plan=previous_time_plan, + include_targets=include_targets, + include_completed_nontarget=include_completed_nontarget, + include_other_time_plans=include_other_time_plans, ) diff --git a/src/core/jupiter/core/time_plans/use_case/load_public.py b/src/core/jupiter/core/time_plans/use_case/load_public.py new file mode 100644 index 000000000..5a213d513 --- /dev/null +++ b/src/core/jupiter/core/time_plans/use_case/load_public.py @@ -0,0 +1,84 @@ +"""Guest readonly use case for loading a published time plan by external id.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.time_plans.domain import TimePlanDomain +from jupiter.core.time_plans.root import TimePlan +from jupiter.core.time_plans.service.load import TimePlanLoadResult, TimePlanLoadService +from jupiter.core.workspaces.root import Workspace +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class TimePlanLoadPublicArgs(UseCaseArgsBase): + """TimePlanLoadPublic args.""" + + external_id: PublishExternalId + + +class TimePlanLoadPublicUseCase( + JupiterGuestReadonlyUseCase[TimePlanLoadPublicArgs, TimePlanLoadResult] +): + """Load a published time plan and its dependent entities by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: TimePlanLoadPublicArgs, + ) -> TimePlanLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.TIME_PLAN.value: + raise InputValidationError( + "The publish entity does not refer to a time plan." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + workspace = await uow.get_for(Workspace).load_by_id( + publish_domain.workspace.ref_id + ) + time_plan_domain = await uow.get_for(TimePlanDomain).load_by_parent( + publish_domain.workspace.ref_id + ) + time_plan = await uow.get_for(TimePlan).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if time_plan.parent_ref_id != time_plan_domain.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace time plan." + ) + + return await TimePlanLoadService().do_it( + uow, + workspace, + time_plan, + allow_archived=False, + include_targets=True, + include_completed_nontarget=False, + include_other_time_plans=False, + include_publish_entity=False, + ) diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx index 9140d750d..b9fc4747b 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -23,6 +23,8 @@ function publishedEntityLocation(externalId: string, owner: string): string { return `/app/public/published/vacation/${externalId}`; case NamedEntityTag.JOURNAL: return `/app/public/published/journal/${externalId}`; + case NamedEntityTag.TIME_PLAN: + return `/app/public/published/time-plan/${externalId}`; default: throw new Response(ReasonPhrases.NOT_FOUND, { status: StatusCodes.NOT_FOUND, diff --git a/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx b/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx new file mode 100644 index 000000000..8bb3a18a8 --- /dev/null +++ b/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx @@ -0,0 +1,186 @@ +import type { + BigPlan, + InboxTask, + TimePlanActivity, + TimePlanActivityDoneness, +} from "@jupiter/webapi-client"; +import { ApiError, TimePlanActivityFeasability } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext, useMemo } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { + isTimePlanActivityBigPlanTarget, + isTimePlanActivityInboxTaskTarget, +} from "@jupiter/core/time_plans/sub/activity/target-wire"; +import { filterActivityByFeasabilityWithParents } from "@jupiter/core/time_plans/sub/activity/root"; +import { entityLinkRefIdFromWire } from "@jupiter/core/common/sub/inbox_tasks/parent-link-namespace"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { TimePlanEditor } from "@jupiter/core/time_plans/component/editor"; +import { allowUserChanges } from "@jupiter/core/time_plans/source"; +import { TimePlanListMergedActivities } from "@jupiter/core/time_plans/component/list-merged-activities"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.timePlans.timePlanLoadPublic({ + external_id: externalId, + }); + + return json({ + timePlan: result.time_plan, + tags: result.tags ?? [], + note: result.note, + activities: result.activities, + aspects: result.aspects, + chapters: result.chapters, + goals: result.goals, + targetInboxTasks: (result.target_inbox_tasks ?? []) as Array<InboxTask>, + targetBigPlans: (result.target_big_plans ?? []) as Array<BigPlan>, + activityDoneness: (result.activity_doneness ?? {}) as Record< + string, + TimePlanActivityDoneness + >, + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedTimePlan() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const { + timePlan, + tags, + note, + activities, + aspects, + chapters, + goals, + targetInboxTasks, + targetBigPlans, + activityDoneness, + } = loaderData; + + const targetInboxTasksByRefId = useMemo( + () => new Map<string, InboxTask>(targetInboxTasks.map((it) => [it.ref_id, it])), + [targetInboxTasks], + ); + const targetBigPlansByRefId = useMemo( + () => new Map<string, BigPlan>(targetBigPlans.map((bp) => [bp.ref_id, bp])), + [targetBigPlans], + ); + const actitiviesByBigPlanRefId = useMemo( + () => + new Map<string, TimePlanActivity>( + activities + .filter((a) => isTimePlanActivityBigPlanTarget(a.target)) + .map((a) => [entityLinkRefIdFromWire(a.target), a]), + ), + [activities], + ); + + const mustDoActivities = filterActivityByFeasabilityWithParents( + activities, + actitiviesByBigPlanRefId, + targetInboxTasksByRefId, + targetBigPlansByRefId, + TimePlanActivityFeasability.MUST_DO, + ); + const niceToHaveActivities = filterActivityByFeasabilityWithParents( + activities, + actitiviesByBigPlanRefId, + targetInboxTasksByRefId, + targetBigPlansByRefId, + TimePlanActivityFeasability.NICE_TO_HAVE, + ); + const stretchActivities = filterActivityByFeasabilityWithParents( + activities, + actitiviesByBigPlanRefId, + targetInboxTasksByRefId, + targetBigPlansByRefId, + TimePlanActivityFeasability.STRETCH, + ); + + return ( + <LeafPanel + key={`published-time-plan-${timePlan.ref_id}`} + fakeKey={`published-time-plan-${timePlan.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <TimePlanEditor + timePlan={timePlan} + tags={tags} + allTags={tags} + aspects={aspects} + chapters={chapters} + goals={goals} + inputsEnabled={false} + corePropertyEditable={allowUserChanges(timePlan.source)} + topLevelInfo={topLevelInfo} + /> + + <SectionCard title="Notes"> + <EntityNoteEditor initialNote={note} inputsEnabled={false} /> + </SectionCard> + + {activities.length > 0 && ( + <SectionCard title="Activities"> + <TimePlanListMergedActivities + mustDoActivities={mustDoActivities} + niceToHaveActivities={niceToHaveActivities} + stretchActivities={stretchActivities} + targetInboxTasksByRefId={targetInboxTasksByRefId} + targetBigPlansByRefId={targetBigPlansByRefId} + activityDoneness={activityDoneness} + timeEventsByRefId={new Map()} + selectedKinds={[]} + selectedFeasabilities={[]} + selectedDoneness={[]} + /> + </SectionCard> + )} + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => + `Could not find published time plan ${params.externalId}!`, + error: (params) => + `There was an error loading published time plan ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/workspace/time-plans/$id.tsx b/src/webui/app/routes/app/workspace/time-plans/$id.tsx index b1f7d393b..0c0113443 100644 --- a/src/webui/app/routes/app/workspace/time-plans/$id.tsx +++ b/src/webui/app/routes/app/workspace/time-plans/$id.tsx @@ -26,7 +26,6 @@ import FlagIcon from "@mui/icons-material/Flag"; import ViewKanbanIcon from "@mui/icons-material/ViewKanban"; import ViewListIcon from "@mui/icons-material/ViewList"; import ViewTimelineIcon from "@mui/icons-material/ViewTimeline"; -import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; @@ -78,7 +77,7 @@ import { EntityNoNothingCard } from "@jupiter/core/infra/component/entity-no-not import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; import { InboxTaskStack } from "@jupiter/core/common/sub/inbox_tasks/component/stack"; import { makeBranchErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; -import { FieldError, GlobalError } from "@jupiter/core/infra/component/errors"; +import { GlobalError } from "@jupiter/core/infra/component/errors"; import { BranchPanel } from "@jupiter/core/infra/component/layout/branch-panel"; import { NestingAwareBlock } from "@jupiter/core/infra/component/layout/nesting-aware-block"; import { TimeAndEffortView } from "@jupiter/core/time_plans/component/time-and-effort-view"; @@ -94,9 +93,7 @@ import { } from "@jupiter/core/infra/component/section-actions"; import { SectionCard } from "@jupiter/core/infra/component/section-card"; import { JournalStack } from "@jupiter/core/journals/component/stack"; -import { PeriodSelect } from "@jupiter/core/common/component/period-select"; -import { entityLinkStd } from "@jupiter/core/common/entity-link"; -import { TagsEditor } from "@jupiter/core/common/sub/tags/component/tags-editor"; +import { TimePlanEditor } from "@jupiter/core/time_plans/component/editor"; import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { useBigScreen } from "@jupiter/core/infra/component/use-big-screen"; import { @@ -111,11 +108,6 @@ import { TimePlanTimelineMergedActivities } from "@jupiter/core/time_plans/compo import { TimePlanTimelineByAspectActivities } from "@jupiter/core/time_plans/component/timeline-by-aspect-activities"; import { TimePlanTimelineByAspectAndGoalActivities } from "@jupiter/core/time_plans/component/timeline-by-aspect-and-goal-activities"; import { TimePlanStack } from "@jupiter/core/time_plans/component/stack"; -import { ChapterMultiSelect } from "#/core/life_plan/sub/chapters/components/multi-select"; -import { AspectMultiSelect } from "#/core/life_plan/sub/aspects/component/multi-select"; -import { aDateToDate } from "#/core/common/adate"; -import { lifePlanBirthdayDate } from "#/core/life_plan/root"; -import { GoalMultiSelect } from "#/core/life_plan/sub/goals/components/multi-select"; import { fixSelectOutputEntityId, selectZod, @@ -175,6 +167,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -256,6 +260,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { subPeriodJournals: journalResult?.sub_period_journals || [], timeEventForInboxTasks: timeEventResult?.entries?.todo_task_entries || [], timeEventForBigPlans: [], + publishEntity: result.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -362,6 +367,30 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/time-plans`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/time-plans/${id}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/time-plans/${id}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/time-plans/${id}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -602,154 +631,28 @@ export default function TimePlanView() { inputsEnabled={inputsEnabled} entityArchived={loaderData.timePlan.archived} returnLocation="/app/workspace/time-plans" + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <NestingAwareBlock shouldHide={shouldShowALeaf}> <GlobalError actionResult={actionData} /> - <SectionCard - title="Properties" - actions={ - <SectionActions - id="time-plan-properties" - topLevelInfo={topLevelInfo} - inputsEnabled={inputsEnabled} - actions={[ - ActionSingle({ - id: "time-plan-change-time-config", - text: "Change Time Config", - value: corePropertyEditable - ? "change-time-config" - : "change-time-config-for-generated", - disabled: !inputsEnabled, - highlight: true, - }), - ]} - /> - } - > - <Stack - direction={isBigScreen ? "row" : "column"} - spacing={2} - useFlexGap - > - <FormControl fullWidth={!isBigScreen}> - <InputLabel id="rightNow" shrink margin="dense"> - The Date - </InputLabel> - <OutlinedInput - type="date" - notched - label="rightNow" - name="rightNow" - readOnly={!inputsEnabled || !corePropertyEditable} - disabled={!inputsEnabled || !corePropertyEditable} - defaultValue={loaderData.timePlan.right_now} - /> - - <FieldError actionResult={actionData} fieldName="/rightNow" /> - </FormControl> - - <FormControl fullWidth={!isBigScreen}> - <PeriodSelect - labelId="period" - label="Period" - name="period" - inputsEnabled={inputsEnabled && corePropertyEditable} - defaultValue={loaderData.timePlan.period} - compact - /> - <FieldError actionResult={actionData} fieldName="/period" /> - <FieldError actionResult={actionData} fieldName="/status" /> - </FormControl> - - <FormControl fullWidth={!isBigScreen}> - <TagsEditor - name="tags_names" - allTags={loaderData.allTags} - defaultValue={loaderData.tags.map((t) => t.ref_id)} - inputsEnabled={inputsEnabled} - owner={entityLinkStd( - NamedEntityTag.TIME_PLAN, - loaderData.timePlan.ref_id, - )} - aloneOnLine={!isBigScreen} - /> - </FormControl> - - {isWorkspaceFeatureAvailable( - topLevelInfo.workspace, - WorkspaceFeature.LIFE_PLAN, - ) && ( - <> - <FormControl - fullWidth={!isBigScreen} - sx={{ width: isBigScreen ? "15%" : "100%" }} - > - <AspectMultiSelect - name="aspectRefIds" - label="Aspect" - inputsEnabled={inputsEnabled} - disabled={false} - allAspects={loaderData.allAspects ?? []} - maxSelections={ - loaderData.lifePlan.time_plan_max_life_plan_links - } - defaultValue={loaderData.aspects.map((p) => p.ref_id)} - /> - <FieldError - actionResult={actionData} - fieldName="/aspectRefIds" - /> - </FormControl> - - <FormControl - fullWidth={!isBigScreen} - sx={{ width: isBigScreen ? "15%" : "100%" }} - > - <ChapterMultiSelect - name="chapterRefIds" - label="Chapter" - inputsEnabled={inputsEnabled} - disabled={false} - allChapters={loaderData.allChapters ?? []} - maxSelections={ - loaderData.lifePlan.time_plan_max_life_plan_links - } - defaultValue={loaderData.chapters.map((c) => c.ref_id)} - birthday={lifePlanBirthdayDate(loaderData.lifePlan)} - today={aDateToDate(topLevelInfo.today)} - allMilestones={loaderData.allMilestones ?? []} - allAspects={loaderData.allAspects ?? []} - /> - <FieldError - actionResult={actionData} - fieldName="/chapterRefIds" - /> - </FormControl> - - <FormControl - fullWidth={!isBigScreen} - sx={{ width: isBigScreen ? "15%" : "100%" }} - > - <GoalMultiSelect - name="goalRefIds" - label="Goal" - inputsEnabled={inputsEnabled} - disabled={false} - allGoals={loaderData.allGoals ?? []} - maxSelections={ - loaderData.lifePlan.time_plan_max_life_plan_links - } - defaultValue={loaderData.goals.map((g) => g.ref_id)} - /> - <FieldError - actionResult={actionData} - fieldName="/goalRefIds" - /> - </FormControl> - </> - )} - </Stack> - </SectionCard> + <TimePlanEditor + timePlan={loaderData.timePlan} + tags={loaderData.tags} + allTags={loaderData.allTags} + aspects={loaderData.aspects} + chapters={loaderData.chapters} + goals={loaderData.goals} + lifePlan={loaderData.lifePlan} + allAspects={loaderData.allAspects} + allChapters={loaderData.allChapters} + allGoals={loaderData.allGoals} + allMilestones={loaderData.allMilestones} + inputsEnabled={inputsEnabled} + corePropertyEditable={corePropertyEditable} + topLevelInfo={topLevelInfo} + actionResult={actionData} + /> <SectionCard title="Notes"> <EntityNoteEditor initialNote={loaderData.note} From f7e54f899b2d1147c0da6b6eb3c1dc9c7bcf4755 Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Sun, 7 Jun 2026 23:15:43 +0300 Subject: [PATCH 06/21] Many new entities --- .../api/metrics/metric_entry_load_public.py | 212 ++++++++++++++ .../schedule_event_full_days_load_public.py | 212 ++++++++++++++ .../schedule_event_in_day_load_public.py | 212 ++++++++++++++ .../smart_list_item_load_public.py | 212 ++++++++++++++ .../jupiter_webapi_client/models/__init__.py | 8 + .../models/metric_entry_load_public_args.py | 62 ++++ .../models/metric_entry_load_result.py | 33 +++ ...hedule_event_full_days_load_public_args.py | 62 ++++ .../schedule_event_full_days_load_result.py | 45 ++- .../schedule_event_in_day_load_public_args.py | 62 ++++ .../schedule_event_in_day_load_result.py | 45 ++- .../smart_list_item_load_public_args.py | 62 ++++ .../models/smart_list_item_load_result.py | 33 +++ gen/ts/webapi-client/gen/index.ts | 4 + .../gen/models/MetricEntryLoadPublicArgs.ts | 12 + .../gen/models/MetricEntryLoadResult.ts | 2 + .../ScheduleEventFullDaysLoadPublicArgs.ts | 12 + .../models/ScheduleEventFullDaysLoadResult.ts | 6 +- .../ScheduleEventInDayLoadPublicArgs.ts | 12 + .../models/ScheduleEventInDayLoadResult.ts | 6 +- .../gen/models/SmartListItemLoadPublicArgs.ts | 12 + .../gen/models/SmartListItemLoadResult.ts | 2 + .../gen/services/MetricsService.ts | 29 ++ .../gen/services/ScheduleService.ts | 58 ++++ .../gen/services/SmartListsService.ts | 29 ++ itests/webui/entities/metrics.test.py | 42 ++- itests/webui/entities/schedules.test.py | 125 +++++++- itests/webui/entities/smart_list.test.py | 41 ++- .../common/sub/publish/sub/entity/root.py | 8 +- .../metrics/sub/entry/component/editor.tsx | 122 ++++++++ .../jupiter/core/metrics/sub/entry/root.py | 4 + .../metrics/sub/entry/service/__init__.py | 1 + .../core/metrics/sub/entry/service/load.py | 98 +++++++ .../core/metrics/sub/entry/use_case/load.py | 81 ++---- .../metrics/sub/entry/use_case/load_public.py | 85 ++++++ .../sub/event_full_days/component/editor.tsx | 233 +++++++++++++++ .../core/schedule/sub/event_full_days/root.py | 5 + .../sub/event_full_days/service/__init__.py | 1 + .../sub/event_full_days/service/load.py | 128 ++++++++ .../sub/event_full_days/use_case/load.py | 101 ++----- .../event_full_days/use_case/load_public.py | 87 ++++++ .../sub/event_in_day/component/editor.tsx | 264 +++++++++++++++++ .../core/schedule/sub/event_in_day/root.py | 4 + .../sub/event_in_day/service/__init__.py | 1 + .../schedule/sub/event_in_day/service/load.py | 123 ++++++++ .../sub/event_in_day/use_case/load.py | 96 ++---- .../sub/event_in_day/use_case/load_public.py | 82 ++++++ .../smart_lists/sub/item/component/editor.tsx | 127 ++++++++ .../jupiter/core/smart_lists/sub/item/root.py | 4 + .../smart_lists/sub/item/service/__init__.py | 1 + .../core/smart_lists/sub/item/service/load.py | 97 ++++++ .../smart_lists/sub/item/use_case/load.py | 85 ++---- .../sub/item/use_case/load_public.py | 85 ++++++ .../app/public/published/$externalId.tsx | 8 + .../published/metric-entry/$externalId.tsx | 100 +++++++ .../schedule-event-full-days/$externalId.tsx | 113 +++++++ .../schedule-event-in-day/$externalId.tsx | 129 ++++++++ .../published/smart-list-item/$externalId.tsx | 100 +++++++ .../calendar/schedule/event-full-days/$id.tsx | 241 ++++----------- .../calendar/schedule/event-in-day/$id.tsx | 275 +++++------------- .../metrics/$id/entries/$entryId.tsx | 152 ++++------ .../app/workspace/smart-lists/$id/$itemId.tsx | 154 ++++------ 62 files changed, 3956 insertions(+), 891 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_full_days_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_in_day_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_item_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_public_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_public_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_public_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_public_args.py create mode 100644 gen/ts/webapi-client/gen/models/MetricEntryLoadPublicArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadPublicArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadPublicArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/SmartListItemLoadPublicArgs.ts create mode 100644 src/core/jupiter/core/metrics/sub/entry/component/editor.tsx create mode 100644 src/core/jupiter/core/metrics/sub/entry/service/__init__.py create mode 100644 src/core/jupiter/core/metrics/sub/entry/service/load.py create mode 100644 src/core/jupiter/core/metrics/sub/entry/use_case/load_public.py create mode 100644 src/core/jupiter/core/schedule/sub/event_full_days/component/editor.tsx create mode 100644 src/core/jupiter/core/schedule/sub/event_full_days/service/__init__.py create mode 100644 src/core/jupiter/core/schedule/sub/event_full_days/service/load.py create mode 100644 src/core/jupiter/core/schedule/sub/event_full_days/use_case/load_public.py create mode 100644 src/core/jupiter/core/schedule/sub/event_in_day/component/editor.tsx create mode 100644 src/core/jupiter/core/schedule/sub/event_in_day/service/__init__.py create mode 100644 src/core/jupiter/core/schedule/sub/event_in_day/service/load.py create mode 100644 src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public.py create mode 100644 src/core/jupiter/core/smart_lists/sub/item/component/editor.tsx create mode 100644 src/core/jupiter/core/smart_lists/sub/item/service/__init__.py create mode 100644 src/core/jupiter/core/smart_lists/sub/item/service/load.py create mode 100644 src/core/jupiter/core/smart_lists/sub/item/use_case/load_public.py create mode 100644 src/webui/app/routes/app/public/published/metric-entry/$externalId.tsx create mode 100644 src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx create mode 100644 src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx create mode 100644 src/webui/app/routes/app/public/published/smart-list-item/$externalId.tsx diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public.py new file mode 100644 index 000000000..647b371b3 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.metric_entry_load_public_args import MetricEntryLoadPublicArgs +from ...models.metric_entry_load_result import MetricEntryLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: MetricEntryLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/metric-entry-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | MetricEntryLoadResult | None: + if response.status_code == 200: + response_200 = MetricEntryLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | MetricEntryLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: MetricEntryLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | MetricEntryLoadResult]: + """Load a published metric entry by publish external id. + + Args: + body (MetricEntryLoadPublicArgs | Unset): MetricEntryLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | MetricEntryLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: MetricEntryLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | MetricEntryLoadResult | None: + """Load a published metric entry by publish external id. + + Args: + body (MetricEntryLoadPublicArgs | Unset): MetricEntryLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | MetricEntryLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: MetricEntryLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | MetricEntryLoadResult]: + """Load a published metric entry by publish external id. + + Args: + body (MetricEntryLoadPublicArgs | Unset): MetricEntryLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | MetricEntryLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: MetricEntryLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | MetricEntryLoadResult | None: + """Load a published metric entry by publish external id. + + Args: + body (MetricEntryLoadPublicArgs | Unset): MetricEntryLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | MetricEntryLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_full_days_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_full_days_load_public.py new file mode 100644 index 000000000..e15d3ca77 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_full_days_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.schedule_event_full_days_load_public_args import ScheduleEventFullDaysLoadPublicArgs +from ...models.schedule_event_full_days_load_result import ScheduleEventFullDaysLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: ScheduleEventFullDaysLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/schedule-event-full-days-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | ScheduleEventFullDaysLoadResult | None: + if response.status_code == 200: + response_200 = ScheduleEventFullDaysLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | ScheduleEventFullDaysLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventFullDaysLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | ScheduleEventFullDaysLoadResult]: + """Load a published schedule full days event by publish external id. + + Args: + body (ScheduleEventFullDaysLoadPublicArgs | Unset): ScheduleEventFullDaysLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | ScheduleEventFullDaysLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventFullDaysLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | ScheduleEventFullDaysLoadResult | None: + """Load a published schedule full days event by publish external id. + + Args: + body (ScheduleEventFullDaysLoadPublicArgs | Unset): ScheduleEventFullDaysLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | ScheduleEventFullDaysLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventFullDaysLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | ScheduleEventFullDaysLoadResult]: + """Load a published schedule full days event by publish external id. + + Args: + body (ScheduleEventFullDaysLoadPublicArgs | Unset): ScheduleEventFullDaysLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | ScheduleEventFullDaysLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventFullDaysLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | ScheduleEventFullDaysLoadResult | None: + """Load a published schedule full days event by publish external id. + + Args: + body (ScheduleEventFullDaysLoadPublicArgs | Unset): ScheduleEventFullDaysLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | ScheduleEventFullDaysLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_in_day_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_in_day_load_public.py new file mode 100644 index 000000000..5e22f5c78 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_in_day_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.schedule_event_in_day_load_public_args import ScheduleEventInDayLoadPublicArgs +from ...models.schedule_event_in_day_load_result import ScheduleEventInDayLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: ScheduleEventInDayLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/schedule-event-in-day-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | ScheduleEventInDayLoadResult | None: + if response.status_code == 200: + response_200 = ScheduleEventInDayLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | ScheduleEventInDayLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventInDayLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | ScheduleEventInDayLoadResult]: + """Load a published schedule event in day by publish external id. + + Args: + body (ScheduleEventInDayLoadPublicArgs | Unset): ScheduleEventInDayLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | ScheduleEventInDayLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventInDayLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | ScheduleEventInDayLoadResult | None: + """Load a published schedule event in day by publish external id. + + Args: + body (ScheduleEventInDayLoadPublicArgs | Unset): ScheduleEventInDayLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | ScheduleEventInDayLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventInDayLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | ScheduleEventInDayLoadResult]: + """Load a published schedule event in day by publish external id. + + Args: + body (ScheduleEventInDayLoadPublicArgs | Unset): ScheduleEventInDayLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | ScheduleEventInDayLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventInDayLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | ScheduleEventInDayLoadResult | None: + """Load a published schedule event in day by publish external id. + + Args: + body (ScheduleEventInDayLoadPublicArgs | Unset): ScheduleEventInDayLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | ScheduleEventInDayLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_item_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_item_load_public.py new file mode 100644 index 000000000..6f829854d --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_item_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.smart_list_item_load_public_args import SmartListItemLoadPublicArgs +from ...models.smart_list_item_load_result import SmartListItemLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: SmartListItemLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/smart-list-item-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | SmartListItemLoadResult | None: + if response.status_code == 200: + response_200 = SmartListItemLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | SmartListItemLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: SmartListItemLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | SmartListItemLoadResult]: + """Load a published smart list item by publish external id. + + Args: + body (SmartListItemLoadPublicArgs | Unset): SmartListItemLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | SmartListItemLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: SmartListItemLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | SmartListItemLoadResult | None: + """Load a published smart list item by publish external id. + + Args: + body (SmartListItemLoadPublicArgs | Unset): SmartListItemLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | SmartListItemLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: SmartListItemLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | SmartListItemLoadResult]: + """Load a published smart list item by publish external id. + + Args: + body (SmartListItemLoadPublicArgs | Unset): SmartListItemLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | SmartListItemLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: SmartListItemLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | SmartListItemLoadResult | None: + """Load a published smart list item by publish external id. + + Args: + body (SmartListItemLoadPublicArgs | Unset): SmartListItemLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | SmartListItemLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index f18a980ff..9939390ce 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -495,6 +495,7 @@ from .metric_entry_create_args import MetricEntryCreateArgs from .metric_entry_create_result import MetricEntryCreateResult from .metric_entry_load_args import MetricEntryLoadArgs +from .metric_entry_load_public_args import MetricEntryLoadPublicArgs from .metric_entry_load_result import MetricEntryLoadResult from .metric_entry_remove_args import MetricEntryRemoveArgs from .metric_entry_update_args import MetricEntryUpdateArgs @@ -651,6 +652,7 @@ from .schedule_event_full_days_create_args import ScheduleEventFullDaysCreateArgs from .schedule_event_full_days_create_result import ScheduleEventFullDaysCreateResult from .schedule_event_full_days_load_args import ScheduleEventFullDaysLoadArgs +from .schedule_event_full_days_load_public_args import ScheduleEventFullDaysLoadPublicArgs from .schedule_event_full_days_load_result import ScheduleEventFullDaysLoadResult from .schedule_event_full_days_remove_args import ScheduleEventFullDaysRemoveArgs from .schedule_event_full_days_update_args import ScheduleEventFullDaysUpdateArgs @@ -663,6 +665,7 @@ from .schedule_event_in_day_create_args import ScheduleEventInDayCreateArgs from .schedule_event_in_day_create_result import ScheduleEventInDayCreateResult from .schedule_event_in_day_load_args import ScheduleEventInDayLoadArgs +from .schedule_event_in_day_load_public_args import ScheduleEventInDayLoadPublicArgs from .schedule_event_in_day_load_result import ScheduleEventInDayLoadResult from .schedule_event_in_day_remove_args import ScheduleEventInDayRemoveArgs from .schedule_event_in_day_update_args import ScheduleEventInDayUpdateArgs @@ -770,6 +773,7 @@ from .smart_list_item_create_args import SmartListItemCreateArgs from .smart_list_item_create_result import SmartListItemCreateResult from .smart_list_item_load_args import SmartListItemLoadArgs +from .smart_list_item_load_public_args import SmartListItemLoadPublicArgs from .smart_list_item_load_result import SmartListItemLoadResult from .smart_list_item_remove_args import SmartListItemRemoveArgs from .smart_list_item_update_args import SmartListItemUpdateArgs @@ -1493,6 +1497,7 @@ "MetricEntryCreateArgs", "MetricEntryCreateResult", "MetricEntryLoadArgs", + "MetricEntryLoadPublicArgs", "MetricEntryLoadResult", "MetricEntryRemoveArgs", "MetricEntryUpdateArgs", @@ -1647,6 +1652,7 @@ "ScheduleEventFullDaysCreateArgs", "ScheduleEventFullDaysCreateResult", "ScheduleEventFullDaysLoadArgs", + "ScheduleEventFullDaysLoadPublicArgs", "ScheduleEventFullDaysLoadResult", "ScheduleEventFullDaysRemoveArgs", "ScheduleEventFullDaysUpdateArgs", @@ -1659,6 +1665,7 @@ "ScheduleEventInDayCreateArgs", "ScheduleEventInDayCreateResult", "ScheduleEventInDayLoadArgs", + "ScheduleEventInDayLoadPublicArgs", "ScheduleEventInDayLoadResult", "ScheduleEventInDayRemoveArgs", "ScheduleEventInDayUpdateArgs", @@ -1764,6 +1771,7 @@ "SmartListItemCreateArgs", "SmartListItemCreateResult", "SmartListItemLoadArgs", + "SmartListItemLoadPublicArgs", "SmartListItemLoadResult", "SmartListItemRemoveArgs", "SmartListItemUpdateArgs", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_public_args.py new file mode 100644 index 000000000..9874e422f --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="MetricEntryLoadPublicArgs") + + +@_attrs_define +class MetricEntryLoadPublicArgs: + """MetricEntryLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + metric_entry_load_public_args = cls( + external_id=external_id, + ) + + metric_entry_load_public_args.additional_properties = d + return metric_entry_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_result.py index a7931ac7a..57e1942df 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_result.py @@ -12,6 +12,7 @@ from ..models.contact import Contact from ..models.metric_entry import MetricEntry from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag @@ -27,16 +28,19 @@ class MetricEntryLoadResult: tags (list[Tag]): contacts (list[Contact]): note (None | Note | Unset): + publish_entity (None | PublishEntity | Unset): """ metric_entry: MetricEntry tags: list[Tag] contacts: list[Contact] note: None | Note | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.note import Note + from ..models.publish_entity import PublishEntity metric_entry = self.metric_entry.to_dict() @@ -58,6 +62,14 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -69,6 +81,8 @@ def to_dict(self) -> dict[str, Any]: ) if note is not UNSET: field_dict["note"] = note + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -77,6 +91,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.contact import Contact from ..models.metric_entry import MetricEntry from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag d = dict(src_dict) @@ -113,11 +128,29 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + metric_entry_load_result = cls( metric_entry=metric_entry, tags=tags, contacts=contacts, note=note, + publish_entity=publish_entity, ) metric_entry_load_result.additional_properties = d diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_public_args.py new file mode 100644 index 000000000..22cb2605b --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ScheduleEventFullDaysLoadPublicArgs") + + +@_attrs_define +class ScheduleEventFullDaysLoadPublicArgs: + """ScheduleEventFullDaysLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + schedule_event_full_days_load_public_args = cls( + external_id=external_id, + ) + + schedule_event_full_days_load_public_args.additional_properties = d + return schedule_event_full_days_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_result.py index 095d1487b..f58028c94 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_result.py @@ -11,7 +11,9 @@ if TYPE_CHECKING: from ..models.contact import Contact from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.schedule_event_full_days import ScheduleEventFullDays + from ..models.schedule_stream_summary import ScheduleStreamSummary from ..models.tag import Tag from ..models.time_event_full_days_block import TimeEventFullDaysBlock @@ -21,25 +23,30 @@ @_attrs_define class ScheduleEventFullDaysLoadResult: - """Result. + """ScheduleEventFullDaysLoadResult. Attributes: schedule_event_full_days (ScheduleEventFullDays): A full day block in a schedule. time_event_full_days_block (TimeEventFullDaysBlock): A full day block of time. tags (list[Tag]): contacts (list[Contact]): + schedule_stream (ScheduleStreamSummary): Summary information about a schedule stream. note (None | Note | Unset): + publish_entity (None | PublishEntity | Unset): """ schedule_event_full_days: ScheduleEventFullDays time_event_full_days_block: TimeEventFullDaysBlock tags: list[Tag] contacts: list[Contact] + schedule_stream: ScheduleStreamSummary note: None | Note | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.note import Note + from ..models.publish_entity import PublishEntity schedule_event_full_days = self.schedule_event_full_days.to_dict() @@ -55,6 +62,8 @@ def to_dict(self) -> dict[str, Any]: contacts_item = contacts_item_data.to_dict() contacts.append(contacts_item) + schedule_stream = self.schedule_stream.to_dict() + note: dict[str, Any] | None | Unset if isinstance(self.note, Unset): note = UNSET @@ -63,6 +72,14 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -71,10 +88,13 @@ def to_dict(self) -> dict[str, Any]: "time_event_full_days_block": time_event_full_days_block, "tags": tags, "contacts": contacts, + "schedule_stream": schedule_stream, } ) if note is not UNSET: field_dict["note"] = note + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -82,7 +102,9 @@ def to_dict(self) -> dict[str, Any]: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.contact import Contact from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.schedule_event_full_days import ScheduleEventFullDays + from ..models.schedule_stream_summary import ScheduleStreamSummary from ..models.tag import Tag from ..models.time_event_full_days_block import TimeEventFullDaysBlock @@ -105,6 +127,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: contacts.append(contacts_item) + schedule_stream = ScheduleStreamSummary.from_dict(d.pop("schedule_stream")) + def _parse_note(data: object) -> None | Note | Unset: if data is None: return data @@ -122,12 +146,31 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + schedule_event_full_days_load_result = cls( schedule_event_full_days=schedule_event_full_days, time_event_full_days_block=time_event_full_days_block, tags=tags, contacts=contacts, + schedule_stream=schedule_stream, note=note, + publish_entity=publish_entity, ) schedule_event_full_days_load_result.additional_properties = d diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_public_args.py new file mode 100644 index 000000000..ed620bcfb --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ScheduleEventInDayLoadPublicArgs") + + +@_attrs_define +class ScheduleEventInDayLoadPublicArgs: + """ScheduleEventInDayLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + schedule_event_in_day_load_public_args = cls( + external_id=external_id, + ) + + schedule_event_in_day_load_public_args.additional_properties = d + return schedule_event_in_day_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_result.py index eaab39095..5e36abe41 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_result.py @@ -11,7 +11,9 @@ if TYPE_CHECKING: from ..models.contact import Contact from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.schedule_event_in_day import ScheduleEventInDay + from ..models.schedule_stream_summary import ScheduleStreamSummary from ..models.tag import Tag from ..models.time_event_in_day_block import TimeEventInDayBlock @@ -21,25 +23,30 @@ @_attrs_define class ScheduleEventInDayLoadResult: - """Result. + """ScheduleEventInDayLoadResult. Attributes: schedule_event_in_day (ScheduleEventInDay): An event in a schedule. time_event_in_day_block (TimeEventInDayBlock): Time event. tags (list[Tag]): contacts (list[Contact]): + schedule_stream (ScheduleStreamSummary): Summary information about a schedule stream. note (None | Note | Unset): + publish_entity (None | PublishEntity | Unset): """ schedule_event_in_day: ScheduleEventInDay time_event_in_day_block: TimeEventInDayBlock tags: list[Tag] contacts: list[Contact] + schedule_stream: ScheduleStreamSummary note: None | Note | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.note import Note + from ..models.publish_entity import PublishEntity schedule_event_in_day = self.schedule_event_in_day.to_dict() @@ -55,6 +62,8 @@ def to_dict(self) -> dict[str, Any]: contacts_item = contacts_item_data.to_dict() contacts.append(contacts_item) + schedule_stream = self.schedule_stream.to_dict() + note: dict[str, Any] | None | Unset if isinstance(self.note, Unset): note = UNSET @@ -63,6 +72,14 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -71,10 +88,13 @@ def to_dict(self) -> dict[str, Any]: "time_event_in_day_block": time_event_in_day_block, "tags": tags, "contacts": contacts, + "schedule_stream": schedule_stream, } ) if note is not UNSET: field_dict["note"] = note + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -82,7 +102,9 @@ def to_dict(self) -> dict[str, Any]: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.contact import Contact from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.schedule_event_in_day import ScheduleEventInDay + from ..models.schedule_stream_summary import ScheduleStreamSummary from ..models.tag import Tag from ..models.time_event_in_day_block import TimeEventInDayBlock @@ -105,6 +127,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: contacts.append(contacts_item) + schedule_stream = ScheduleStreamSummary.from_dict(d.pop("schedule_stream")) + def _parse_note(data: object) -> None | Note | Unset: if data is None: return data @@ -122,12 +146,31 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + schedule_event_in_day_load_result = cls( schedule_event_in_day=schedule_event_in_day, time_event_in_day_block=time_event_in_day_block, tags=tags, contacts=contacts, + schedule_stream=schedule_stream, note=note, + publish_entity=publish_entity, ) schedule_event_in_day_load_result.additional_properties = d diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_public_args.py new file mode 100644 index 000000000..24869d23f --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="SmartListItemLoadPublicArgs") + + +@_attrs_define +class SmartListItemLoadPublicArgs: + """SmartListItemLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + smart_list_item_load_public_args = cls( + external_id=external_id, + ) + + smart_list_item_load_public_args.additional_properties = d + return smart_list_item_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_result.py index a124c0e1e..971d4ccfc 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_result.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from ..models.contact import Contact from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.smart_list_item import SmartListItem from ..models.tag import Tag @@ -27,16 +28,19 @@ class SmartListItemLoadResult: generic_tags (list[Tag]): contacts (list[Contact]): note (None | Note | Unset): + publish_entity (None | PublishEntity | Unset): """ item: SmartListItem generic_tags: list[Tag] contacts: list[Contact] note: None | Note | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.note import Note + from ..models.publish_entity import PublishEntity item = self.item.to_dict() @@ -58,6 +62,14 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -69,6 +81,8 @@ def to_dict(self) -> dict[str, Any]: ) if note is not UNSET: field_dict["note"] = note + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -76,6 +90,7 @@ def to_dict(self) -> dict[str, Any]: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.contact import Contact from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.smart_list_item import SmartListItem from ..models.tag import Tag @@ -113,11 +128,29 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + smart_list_item_load_result = cls( item=item, generic_tags=generic_tags, contacts=contacts, note=note, + publish_entity=publish_entity, ) smart_list_item_load_result.additional_properties = d diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index 30cbbb2f4..e9040fc1a 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -400,6 +400,7 @@ export type { MetricEntryArchiveArgs } from './models/MetricEntryArchiveArgs'; export type { MetricEntryCreateArgs } from './models/MetricEntryCreateArgs'; export type { MetricEntryCreateResult } from './models/MetricEntryCreateResult'; export type { MetricEntryLoadArgs } from './models/MetricEntryLoadArgs'; +export type { MetricEntryLoadPublicArgs } from './models/MetricEntryLoadPublicArgs'; export type { MetricEntryLoadResult } from './models/MetricEntryLoadResult'; export type { MetricEntryRemoveArgs } from './models/MetricEntryRemoveArgs'; export type { MetricEntryUpdateArgs } from './models/MetricEntryUpdateArgs'; @@ -532,6 +533,7 @@ export type { ScheduleEventFullDaysChangeScheduleStreamArgs } from './models/Sch export type { ScheduleEventFullDaysCreateArgs } from './models/ScheduleEventFullDaysCreateArgs'; export type { ScheduleEventFullDaysCreateResult } from './models/ScheduleEventFullDaysCreateResult'; export type { ScheduleEventFullDaysLoadArgs } from './models/ScheduleEventFullDaysLoadArgs'; +export type { ScheduleEventFullDaysLoadPublicArgs } from './models/ScheduleEventFullDaysLoadPublicArgs'; export type { ScheduleEventFullDaysLoadResult } from './models/ScheduleEventFullDaysLoadResult'; export type { ScheduleEventFullDaysName } from './models/ScheduleEventFullDaysName'; export type { ScheduleEventFullDaysRemoveArgs } from './models/ScheduleEventFullDaysRemoveArgs'; @@ -542,6 +544,7 @@ export type { ScheduleEventInDayChangeScheduleStreamArgs } from './models/Schedu export type { ScheduleEventInDayCreateArgs } from './models/ScheduleEventInDayCreateArgs'; export type { ScheduleEventInDayCreateResult } from './models/ScheduleEventInDayCreateResult'; export type { ScheduleEventInDayLoadArgs } from './models/ScheduleEventInDayLoadArgs'; +export type { ScheduleEventInDayLoadPublicArgs } from './models/ScheduleEventInDayLoadPublicArgs'; export type { ScheduleEventInDayLoadResult } from './models/ScheduleEventInDayLoadResult'; export type { ScheduleEventInDayName } from './models/ScheduleEventInDayName'; export type { ScheduleEventInDayRemoveArgs } from './models/ScheduleEventInDayRemoveArgs'; @@ -637,6 +640,7 @@ export type { SmartListItemArchiveArgs } from './models/SmartListItemArchiveArgs export type { SmartListItemCreateArgs } from './models/SmartListItemCreateArgs'; export type { SmartListItemCreateResult } from './models/SmartListItemCreateResult'; export type { SmartListItemLoadArgs } from './models/SmartListItemLoadArgs'; +export type { SmartListItemLoadPublicArgs } from './models/SmartListItemLoadPublicArgs'; export type { SmartListItemLoadResult } from './models/SmartListItemLoadResult'; export type { SmartListItemName } from './models/SmartListItemName'; export type { SmartListItemRemoveArgs } from './models/SmartListItemRemoveArgs'; diff --git a/gen/ts/webapi-client/gen/models/MetricEntryLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/MetricEntryLoadPublicArgs.ts new file mode 100644 index 000000000..f061c374b --- /dev/null +++ b/gen/ts/webapi-client/gen/models/MetricEntryLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * MetricEntryLoadPublic args. + */ +export type MetricEntryLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/MetricEntryLoadResult.ts b/gen/ts/webapi-client/gen/models/MetricEntryLoadResult.ts index 79a9f4dac..58b7f92d2 100644 --- a/gen/ts/webapi-client/gen/models/MetricEntryLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/MetricEntryLoadResult.ts @@ -5,6 +5,7 @@ import type { Contact } from './Contact'; import type { MetricEntry } from './MetricEntry'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { Tag } from './Tag'; /** * MetricEntryLoadResult. @@ -14,5 +15,6 @@ export type MetricEntryLoadResult = { tags: Array<Tag>; contacts: Array<Contact>; note?: (Note | null); + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadPublicArgs.ts new file mode 100644 index 000000000..8bc79b253 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * ScheduleEventFullDaysLoadPublic args. + */ +export type ScheduleEventFullDaysLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadResult.ts b/gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadResult.ts index dc2a1e02a..934f1a464 100644 --- a/gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadResult.ts @@ -4,11 +4,13 @@ /* eslint-disable */ import type { Contact } from './Contact'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { ScheduleEventFullDays } from './ScheduleEventFullDays'; +import type { ScheduleStreamSummary } from './ScheduleStreamSummary'; import type { Tag } from './Tag'; import type { TimeEventFullDaysBlock } from './TimeEventFullDaysBlock'; /** - * Result. + * ScheduleEventFullDaysLoadResult. */ export type ScheduleEventFullDaysLoadResult = { schedule_event_full_days: ScheduleEventFullDays; @@ -16,5 +18,7 @@ export type ScheduleEventFullDaysLoadResult = { note?: (Note | null); tags: Array<Tag>; contacts: Array<Contact>; + schedule_stream: ScheduleStreamSummary; + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadPublicArgs.ts new file mode 100644 index 000000000..04f2db39e --- /dev/null +++ b/gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * ScheduleEventInDayLoadPublic args. + */ +export type ScheduleEventInDayLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadResult.ts b/gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadResult.ts index cd8fd24fc..96e1952eb 100644 --- a/gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadResult.ts @@ -4,11 +4,13 @@ /* eslint-disable */ import type { Contact } from './Contact'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { ScheduleEventInDay } from './ScheduleEventInDay'; +import type { ScheduleStreamSummary } from './ScheduleStreamSummary'; import type { Tag } from './Tag'; import type { TimeEventInDayBlock } from './TimeEventInDayBlock'; /** - * Result. + * ScheduleEventInDayLoadResult. */ export type ScheduleEventInDayLoadResult = { schedule_event_in_day: ScheduleEventInDay; @@ -16,5 +18,7 @@ export type ScheduleEventInDayLoadResult = { note?: (Note | null); tags: Array<Tag>; contacts: Array<Contact>; + schedule_stream: ScheduleStreamSummary; + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/models/SmartListItemLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/SmartListItemLoadPublicArgs.ts new file mode 100644 index 000000000..f31da4290 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/SmartListItemLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * SmartListItemLoadPublic args. + */ +export type SmartListItemLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/SmartListItemLoadResult.ts b/gen/ts/webapi-client/gen/models/SmartListItemLoadResult.ts index 3eb04f171..8ede9a7e5 100644 --- a/gen/ts/webapi-client/gen/models/SmartListItemLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/SmartListItemLoadResult.ts @@ -4,6 +4,7 @@ /* eslint-disable */ import type { Contact } from './Contact'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { SmartListItem } from './SmartListItem'; import type { Tag } from './Tag'; /** @@ -14,5 +15,6 @@ export type SmartListItemLoadResult = { generic_tags: Array<Tag>; contacts: Array<Contact>; note?: (Note | null); + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/services/MetricsService.ts b/gen/ts/webapi-client/gen/services/MetricsService.ts index 939b490fe..40e534d7b 100644 --- a/gen/ts/webapi-client/gen/services/MetricsService.ts +++ b/gen/ts/webapi-client/gen/services/MetricsService.ts @@ -9,6 +9,7 @@ import type { MetricEntryArchiveArgs } from '../models/MetricEntryArchiveArgs'; import type { MetricEntryCreateArgs } from '../models/MetricEntryCreateArgs'; import type { MetricEntryCreateResult } from '../models/MetricEntryCreateResult'; import type { MetricEntryLoadArgs } from '../models/MetricEntryLoadArgs'; +import type { MetricEntryLoadPublicArgs } from '../models/MetricEntryLoadPublicArgs'; import type { MetricEntryLoadResult } from '../models/MetricEntryLoadResult'; import type { MetricEntryRemoveArgs } from '../models/MetricEntryRemoveArgs'; import type { MetricEntryUpdateArgs } from '../models/MetricEntryUpdateArgs'; @@ -109,6 +110,34 @@ export class MetricsService { }, }); } + /** + * Load a published metric entry by publish external id. + * @param requestBody The input data + * @returns MetricEntryLoadResult Successful response + * @throws ApiError + */ + public metricEntryLoadPublic( + requestBody?: MetricEntryLoadPublicArgs, + ): CancelablePromise<MetricEntryLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/metric-entry-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * The command for removing a metric entry. * @param requestBody The input data diff --git a/gen/ts/webapi-client/gen/services/ScheduleService.ts b/gen/ts/webapi-client/gen/services/ScheduleService.ts index b3b05ca65..4c1a991e5 100644 --- a/gen/ts/webapi-client/gen/services/ScheduleService.ts +++ b/gen/ts/webapi-client/gen/services/ScheduleService.ts @@ -7,6 +7,7 @@ import type { ScheduleEventFullDaysChangeScheduleStreamArgs } from '../models/Sc import type { ScheduleEventFullDaysCreateArgs } from '../models/ScheduleEventFullDaysCreateArgs'; import type { ScheduleEventFullDaysCreateResult } from '../models/ScheduleEventFullDaysCreateResult'; import type { ScheduleEventFullDaysLoadArgs } from '../models/ScheduleEventFullDaysLoadArgs'; +import type { ScheduleEventFullDaysLoadPublicArgs } from '../models/ScheduleEventFullDaysLoadPublicArgs'; import type { ScheduleEventFullDaysLoadResult } from '../models/ScheduleEventFullDaysLoadResult'; import type { ScheduleEventFullDaysRemoveArgs } from '../models/ScheduleEventFullDaysRemoveArgs'; import type { ScheduleEventFullDaysUpdateArgs } from '../models/ScheduleEventFullDaysUpdateArgs'; @@ -15,6 +16,7 @@ import type { ScheduleEventInDayChangeScheduleStreamArgs } from '../models/Sched import type { ScheduleEventInDayCreateArgs } from '../models/ScheduleEventInDayCreateArgs'; import type { ScheduleEventInDayCreateResult } from '../models/ScheduleEventInDayCreateResult'; import type { ScheduleEventInDayLoadArgs } from '../models/ScheduleEventInDayLoadArgs'; +import type { ScheduleEventInDayLoadPublicArgs } from '../models/ScheduleEventInDayLoadPublicArgs'; import type { ScheduleEventInDayLoadResult } from '../models/ScheduleEventInDayLoadResult'; import type { ScheduleEventInDayRemoveArgs } from '../models/ScheduleEventInDayRemoveArgs'; import type { ScheduleEventInDayUpdateArgs } from '../models/ScheduleEventInDayUpdateArgs'; @@ -159,6 +161,34 @@ export class ScheduleService { }, }); } + /** + * Load a published schedule full days event by publish external id. + * @param requestBody The input data + * @returns ScheduleEventFullDaysLoadResult Successful response + * @throws ApiError + */ + public scheduleEventFullDaysLoadPublic( + requestBody?: ScheduleEventFullDaysLoadPublicArgs, + ): CancelablePromise<ScheduleEventFullDaysLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/schedule-event-full-days-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * Use case for removing a full day event. * @param requestBody The input data @@ -327,6 +357,34 @@ export class ScheduleService { }, }); } + /** + * Load a published schedule event in day by publish external id. + * @param requestBody The input data + * @returns ScheduleEventInDayLoadResult Successful response + * @throws ApiError + */ + public scheduleEventInDayLoadPublic( + requestBody?: ScheduleEventInDayLoadPublicArgs, + ): CancelablePromise<ScheduleEventInDayLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/schedule-event-in-day-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * Use case for removing a schedule in day event. * @param requestBody The input data diff --git a/gen/ts/webapi-client/gen/services/SmartListsService.ts b/gen/ts/webapi-client/gen/services/SmartListsService.ts index 33defa932..e05d88101 100644 --- a/gen/ts/webapi-client/gen/services/SmartListsService.ts +++ b/gen/ts/webapi-client/gen/services/SmartListsService.ts @@ -11,6 +11,7 @@ import type { SmartListItemArchiveArgs } from '../models/SmartListItemArchiveArg import type { SmartListItemCreateArgs } from '../models/SmartListItemCreateArgs'; import type { SmartListItemCreateResult } from '../models/SmartListItemCreateResult'; import type { SmartListItemLoadArgs } from '../models/SmartListItemLoadArgs'; +import type { SmartListItemLoadPublicArgs } from '../models/SmartListItemLoadPublicArgs'; import type { SmartListItemLoadResult } from '../models/SmartListItemLoadResult'; import type { SmartListItemRemoveArgs } from '../models/SmartListItemRemoveArgs'; import type { SmartListItemUpdateArgs } from '../models/SmartListItemUpdateArgs'; @@ -106,6 +107,34 @@ export class SmartListsService { }, }); } + /** + * Load a published smart list item by publish external id. + * @param requestBody The input data + * @returns SmartListItemLoadResult Successful response + * @throws ApiError + */ + public smartListItemLoadPublic( + requestBody?: SmartListItemLoadPublicArgs, + ): CancelablePromise<SmartListItemLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/smart-list-item-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * The command for removing a smart list item. * @param requestBody The input data diff --git a/itests/webui/entities/metrics.test.py b/itests/webui/entities/metrics.test.py index f9f4e2804..660ce433d 100644 --- a/itests/webui/entities/metrics.test.py +++ b/itests/webui/entities/metrics.test.py @@ -1,5 +1,7 @@ """Tests about metrics.""" +import re + import pytest from jupiter_webapi_client.api.metrics.metric_create import ( sync_detailed as metric_create_sync, @@ -30,7 +32,7 @@ ) from playwright.sync_api import Page, expect -from itests.helpers import get_parsed_from_response +from itests.helpers import get_parsed_from_response, open_leaf_publish_panel @pytest.fixture(autouse=True, scope="module") @@ -145,3 +147,41 @@ def test_webui_metric_view_one_entries( page.wait_for_selector("#branch-panel") expect(page.locator(f"#metric-entry-{metric_entry1.ref_id}")).to_contain_text("10") expect(page.locator(f"#metric-entry-{metric_entry2.ref_id}")).to_contain_text("25") + + +def test_webui_metric_entry_publish_and_view_public( + page: Page, create_metric, create_metric_entry +) -> None: + metric = create_metric("Publish Metric", metric_unit=MetricUnit.COUNT) + entry = create_metric_entry(metric.ref_id, 25.0, "2024-01-15") + page.goto(f"/app/workspace/metrics/{metric.ref_id}/entries/{entry.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "MetricEntry-publish") + page.locator("button[id='MetricEntry-publish-create']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/metrics/{metric.ref_id}/entries/{entry.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "MetricEntry-publish") + expect(page.locator("#MetricEntry-publish")).to_contain_text("draft") + + page.locator("button[id='MetricEntry-publish-toggle-status']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/metrics/{metric.ref_id}/entries/{entry.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "MetricEntry-publish") + expect(page.locator("#MetricEntry-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/metric-entry/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="collectionTime"]')).to_have_value("2024-01-15") + expect(page.locator('input[name="value"]')).to_have_value("25") diff --git a/itests/webui/entities/schedules.test.py b/itests/webui/entities/schedules.test.py index 4ad26a7d9..0c6a46c85 100644 --- a/itests/webui/entities/schedules.test.py +++ b/itests/webui/entities/schedules.test.py @@ -4,6 +4,9 @@ import pendulum import pytest +from jupiter_webapi_client.api.schedule.schedule_event_full_days_create import ( + sync_detailed as schedule_event_full_days_create_sync, +) from jupiter_webapi_client.api.schedule.schedule_event_in_day_create import ( sync_detailed as schedule_event_in_day_create_sync, ) @@ -14,6 +17,13 @@ sync_detailed as workspace_set_feature_sync, ) from jupiter_webapi_client.client import AuthenticatedClient +from jupiter_webapi_client.models.schedule_event_full_days import ScheduleEventFullDays +from jupiter_webapi_client.models.schedule_event_full_days_create_args import ( + ScheduleEventFullDaysCreateArgs, +) +from jupiter_webapi_client.models.schedule_event_full_days_create_result import ( + ScheduleEventFullDaysCreateResult, +) from jupiter_webapi_client.models.schedule_event_in_day import ScheduleEventInDay from jupiter_webapi_client.models.schedule_event_in_day_create_args import ( ScheduleEventInDayCreateArgs, @@ -35,7 +45,7 @@ ) from playwright.sync_api import Page, expect -from itests.helpers import get_parsed_from_response +from itests.helpers import get_parsed_from_response, open_leaf_publish_panel @pytest.fixture(autouse=True, scope="module") @@ -104,6 +114,30 @@ def _create_schedule_event_in_day( return _create_schedule_event_in_day +@pytest.fixture(autouse=True, scope="module") +def create_schedule_event_full_days(logged_in_client: AuthenticatedClient): + def _create_schedule_event_full_days( + schedule_stream_ref_id: str, + name: str, + start_date: str, + duration_days: int = 3, + ) -> ScheduleEventFullDays: + result = schedule_event_full_days_create_sync( + client=logged_in_client, + body=ScheduleEventFullDaysCreateArgs( + schedule_stream_ref_id=schedule_stream_ref_id, + name=name, + start_date=start_date, + duration_days=duration_days, + ), + ) + return get_parsed_from_response( + ScheduleEventFullDaysCreateResult, result + ).new_schedule_event_full_days + + return _create_schedule_event_full_days + + def test_webui_schedule_view_empty_calendar(page: Page) -> None: page.goto("/app/workspace/calendar") @@ -156,3 +190,92 @@ def test_webui_schedule_view_with_events( expect( page.locator(f"#schedule-event-in-day-block-{event3.ref_id}") ).to_contain_text(re.compile(r".*Aspect.*")) + + +def test_webui_schedule_event_in_day_publish_and_view_public( + page: Page, create_schedule_stream, create_schedule_event_in_day +) -> None: + schedule_stream = create_schedule_stream("Publish Stream") + event = create_schedule_event_in_day( + schedule_stream.ref_id, + "Published In Day Event", + "2024-07-01", + "09:00", + 60, + ) + page.goto(f"/app/workspace/calendar/schedule/event-in-day/{event.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "ScheduleEventInDay-publish") + page.locator("button[id='ScheduleEventInDay-publish-create']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/calendar/schedule/event-in-day/{event.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "ScheduleEventInDay-publish") + expect(page.locator("#ScheduleEventInDay-publish")).to_contain_text("draft") + + page.locator("button[id='ScheduleEventInDay-publish-toggle-status']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/calendar/schedule/event-in-day/{event.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "ScheduleEventInDay-publish") + expect(page.locator("#ScheduleEventInDay-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/schedule-event-in-day/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="name"]')).to_have_value("Published In Day Event") + expect(page.locator('input[name="startDate"]')).to_have_value("2024-07-01") + expect(page.locator('input[name="startTimeInDay"]')).to_have_value("09:00") + + +def test_webui_schedule_event_full_days_publish_and_view_public( + page: Page, create_schedule_stream, create_schedule_event_full_days +) -> None: + schedule_stream = create_schedule_stream("Publish Full Days Stream") + event = create_schedule_event_full_days( + schedule_stream.ref_id, + "Published Full Days Event", + "2024-07-01", + 3, + ) + page.goto(f"/app/workspace/calendar/schedule/event-full-days/{event.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "ScheduleEventFullDays-publish") + page.locator("button[id='ScheduleEventFullDays-publish-create']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/calendar/schedule/event-full-days/{event.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "ScheduleEventFullDays-publish") + expect(page.locator("#ScheduleEventFullDays-publish")).to_contain_text("draft") + + page.locator("button[id='ScheduleEventFullDays-publish-toggle-status']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/calendar/schedule/event-full-days/{event.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "ScheduleEventFullDays-publish") + expect(page.locator("#ScheduleEventFullDays-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/schedule-event-full-days/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="name"]')).to_have_value("Published Full Days Event") + expect(page.locator('input[name="startDate"]')).to_have_value("2024-07-01") + expect(page.locator('input[name="durationDays"]')).to_have_value("3") diff --git a/itests/webui/entities/smart_list.test.py b/itests/webui/entities/smart_list.test.py index e13805ccb..6bec41fcf 100644 --- a/itests/webui/entities/smart_list.test.py +++ b/itests/webui/entities/smart_list.test.py @@ -1,5 +1,7 @@ """Tests about smart lists.""" +import re + import pytest from jupiter_webapi_client.api.smart_lists.smart_list_create import ( sync_detailed as smart_list_create_sync, @@ -27,7 +29,7 @@ ) from playwright.sync_api import Page, expect -from itests.helpers import get_parsed_from_response +from itests.helpers import get_parsed_from_response, open_leaf_publish_panel @pytest.fixture(autouse=True, scope="module") @@ -131,3 +133,40 @@ def test_webui_smart_list_view_one_items( expect(page.locator(f"#smart-list-item-{smart_list_item2.ref_id}")).to_contain_text( "Smart List Item 2" ) + + +def test_webui_smart_list_item_publish_and_view_public( + page: Page, create_smart_list, create_smart_list_item +) -> None: + smart_list = create_smart_list("Publish Smart List") + item = create_smart_list_item("Published Smart List Item", smart_list.ref_id) + page.goto(f"/app/workspace/smart-lists/{smart_list.ref_id}/{item.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "SmartListItem-publish") + page.locator("button[id='SmartListItem-publish-create']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/smart-lists/{smart_list.ref_id}/{item.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "SmartListItem-publish") + expect(page.locator("#SmartListItem-publish")).to_contain_text("draft") + + page.locator("button[id='SmartListItem-publish-toggle-status']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/smart-lists/{smart_list.ref_id}/{item.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "SmartListItem-publish") + expect(page.locator("#SmartListItem-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/smart-list-item/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="name"]')).to_have_value("Published Smart List Item") diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index 6c58c2210..1c0e24076 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -29,8 +29,8 @@ NamedEntityTag.TODO_TASK.value, # done NamedEntityTag.TIME_PLAN.value, # done NamedEntityTag.SCHEDULE_STREAM.value, - NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, - NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, + NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, # done + NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, # done NamedEntityTag.HABIT.value, NamedEntityTag.CHORE.value, NamedEntityTag.BIG_PLAN.value, @@ -39,9 +39,9 @@ NamedEntityTag.JOURNAL.value, # done NamedEntityTag.VACATION.value, # done NamedEntityTag.SMART_LIST.value, - NamedEntityTag.SMART_LIST_ITEM.value, + NamedEntityTag.SMART_LIST_ITEM.value, # done NamedEntityTag.METRIC.value, - NamedEntityTag.METRIC_ENTRY.value, + NamedEntityTag.METRIC_ENTRY.value, # done NamedEntityTag.PERSON.value, } ) diff --git a/src/core/jupiter/core/metrics/sub/entry/component/editor.tsx b/src/core/jupiter/core/metrics/sub/entry/component/editor.tsx new file mode 100644 index 000000000..30d6095cd --- /dev/null +++ b/src/core/jupiter/core/metrics/sub/entry/component/editor.tsx @@ -0,0 +1,122 @@ +import type { Contact, MetricEntry, Tag } from "@jupiter/webapi-client"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; +import { aDateToDate } from "#/core/common/adate"; +import { entityLinkStd } from "#/core/common/entity-link"; +import { TimeDiffTag } from "#/core/common/component/time-diff-tag"; +import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; +import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; +import type { ActionResult } from "#/core/infra/action-result"; +import { FieldError } from "#/core/infra/component/errors"; +import { + ActionSingle, + SectionActions, +} from "#/core/infra/component/section-actions"; +import { SectionCard } from "#/core/infra/component/section-card"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import type { TopLevelInfo } from "#/core/infra/top-level-context"; + +interface MetricEntryEditorProps { + metricEntry: MetricEntry; + tags: Array<Tag>; + contacts: Array<Contact>; + allTags: Array<Tag>; + allContacts: Array<Contact>; + inputsEnabled: boolean; + topLevelInfo: TopLevelInfo; + actionResult?: ActionResult<unknown>; +} + +export function MetricEntryEditor(props: MetricEntryEditorProps) { + const isBigScreen = useBigScreen(); + const { metricEntry, tags, contacts, allTags, allContacts } = props; + + return ( + <SectionCard + title="Properties" + actions={ + props.inputsEnabled ? ( + <SectionActions + id="metric-entry-properties" + topLevelInfo={props.topLevelInfo} + inputsEnabled={props.inputsEnabled} + actions={[ + ActionSingle({ + id: "metric-entry-update", + text: "Save", + value: "update", + highlight: true, + }), + ]} + /> + ) : undefined + } + > + <Stack direction={isBigScreen ? "row" : "column"} spacing={2}> + <TimeDiffTag + today={props.topLevelInfo.today} + labelPrefix="Collected" + collectionTime={metricEntry.collection_time} + /> + + <FormControl fullWidth sx={{ flexGrow: 1 }}> + <TagsEditor + name="tags" + label={null} + aloneOnLine + allTags={allTags} + defaultValue={tags.map((tag) => tag.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.METRIC_ENTRY, metricEntry.ref_id)} + /> + </FormControl> + + <FormControl fullWidth sx={{ flexGrow: 1 }}> + <ContactsEditor + name="contacts_names" + label={null} + aloneOnLine + allContacts={allContacts} + defaultValue={contacts.map((contact) => contact.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.METRIC_ENTRY, metricEntry.ref_id)} + /> + </FormControl> + </Stack> + + <FormControl fullWidth> + <InputLabel id="collectionTime" shrink> + Collection Time + </InputLabel> + <OutlinedInput + type="date" + notched + label="collectionTime" + defaultValue={ + metricEntry.collection_time + ? aDateToDate(metricEntry.collection_time).toFormat("yyyy-MM-dd") + : undefined + } + name="collectionTime" + readOnly={!props.inputsEnabled} + disabled={!props.inputsEnabled} + /> + + <FieldError actionResult={props.actionResult} fieldName="/collection_time" /> + </FormControl> + + <FormControl fullWidth> + <InputLabel id="value">Value</InputLabel> + <OutlinedInput + type="number" + inputProps={{ step: "any" }} + label="Value" + name="value" + readOnly={!props.inputsEnabled} + defaultValue={metricEntry.value} + /> + <FieldError actionResult={props.actionResult} fieldName="/value" /> + </FormControl> + </SectionCard> + ); +} diff --git a/src/core/jupiter/core/metrics/sub/entry/root.py b/src/core/jupiter/core/metrics/sub/entry/root.py index 2ad2963d9..fec83e4d3 100644 --- a/src/core/jupiter/core/metrics/sub/entry/root.py +++ b/src/core/jupiter/core/metrics/sub/entry/root.py @@ -1,6 +1,7 @@ """A metric entry.""" from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.framework.base.adate import ADate @@ -31,6 +32,9 @@ class MetricEntry(LeafEntity): TagLink, owner=IsEntityLinkStd(NamedEntityTag.METRIC_ENTRY.value) ) note = OwnsAtMostOne(Note, owner=IsEntityLinkStd(NamedEntityTag.METRIC_ENTRY.value)) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.METRIC_ENTRY.value) + ) @staticmethod @create_entity_action diff --git a/src/core/jupiter/core/metrics/sub/entry/service/__init__.py b/src/core/jupiter/core/metrics/sub/entry/service/__init__.py new file mode 100644 index 000000000..86469ef04 --- /dev/null +++ b/src/core/jupiter/core/metrics/sub/entry/service/__init__.py @@ -0,0 +1 @@ +"""Services for metric entries.""" diff --git a/src/core/jupiter/core/metrics/sub/entry/service/load.py b/src/core/jupiter/core/metrics/sub/entry/service/load.py new file mode 100644 index 000000000..ba63d1626 --- /dev/null +++ b/src/core/jupiter/core/metrics/sub/entry/service/load.py @@ -0,0 +1,98 @@ +"""Shared service for loading a metric entry.""" + +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.metrics.sub.entry.root import MetricEntry +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result + + +@use_case_result +class MetricEntryLoadResult(UseCaseResultBase): + """MetricEntryLoadResult.""" + + metric_entry: MetricEntry + tags: list[Tag] + contacts: list[Contact] + note: Note | None + publish_entity: PublishEntity | None + + +class MetricEntryLoadService: + """Shared service for loading a metric entry.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + metric_entry: MetricEntry, + *, + allow_archived: bool = False, + include_publish_entity: bool = True, + ) -> MetricEntryLoadResult: + """Load a metric entry and its dependent entities.""" + metric_entry = await uow.get_for(MetricEntry).load_by_id( + metric_entry.ref_id, allow_archived=allow_archived + ) + + note = await uow.get(NoteRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.METRIC_ENTRY.value, metric_entry.ref_id), + allow_archived=allow_archived, + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std( + NamedEntityTag.METRIC_ENTRY.value, metric_entry.ref_id + ), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace_ref_id, + ) + contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.METRIC_ENTRY.value, metric_entry.ref_id), + ) + if contact_link is not None: + contacts = await uow.get_for(Contact).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + ref_id=contact_link.contacts_ref_ids, + ) + else: + contacts = [] + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std(NamedEntityTag.METRIC_ENTRY.value, metric_entry.ref_id), + allow_archived=allow_archived, + ) + + return MetricEntryLoadResult( + metric_entry=metric_entry, + tags=tags, + contacts=contacts, + note=note, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/metrics/sub/entry/use_case/load.py b/src/core/jupiter/core/metrics/sub/entry/use_case/load.py index 07655328a..6d904ec55 100644 --- a/src/core/jupiter/core/metrics/sub/entry/use_case/load.py +++ b/src/core/jupiter/core/metrics/sub/entry/use_case/load.py @@ -1,30 +1,25 @@ """Use case for loading a metric entry.""" -from jupiter.core.common.sub.contacts.root import ContactDomain -from jupiter.core.common.sub.contacts.sub.contact.root import Contact -from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository -from jupiter.core.common.sub.notes.root import Note, NoteRepository -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature from jupiter.core.metrics.sub.entry.root import MetricEntry -from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.metrics.sub.entry.service.load import ( + MetricEntryLoadResult, + MetricEntryLoadService, +) from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.storage.repository import DomainUnitOfWork -from jupiter.framework.use_case import ( - readonly_use_case, -) -from jupiter.framework.use_case_io import ( - UseCaseArgsBase, - UseCaseResultBase, - use_case_args, - use_case_result, -) +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + +__all__ = [ + "MetricEntryLoadArgs", + "MetricEntryLoadResult", + "MetricEntryLoadUseCase", +] @use_case_args @@ -35,16 +30,6 @@ class MetricEntryLoadArgs(UseCaseArgsBase): allow_archived: bool | None -@use_case_result -class MetricEntryLoadResult(UseCaseResultBase): - """MetricEntryLoadResult.""" - - metric_entry: MetricEntry - tags: list[Tag] - contacts: list[Contact] - note: Note | None - - @readonly_use_case(WorkspaceFeature.METRICS) class MetricEntryLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[ @@ -61,46 +46,14 @@ async def _perform_transactional_read( ) -> MetricEntryLoadResult: """Execute the command's action.""" allow_archived = args.allow_archived or False + metric_entry = await uow.get_for(MetricEntry).load_by_id( args.ref_id, allow_archived=allow_archived ) - note = await uow.get(NoteRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.METRIC_ENTRY.value, metric_entry.ref_id), - allow_archived=allow_archived, - ) - - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std( - NamedEntityTag.METRIC_ENTRY.value, metric_entry.ref_id - ), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - contact_domain = await uow.get_for(ContactDomain).load_by_parent( + return await MetricEntryLoadService().do_it( + uow, context.workspace.ref_id, - ) - contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.METRIC_ENTRY.value, metric_entry.ref_id), - ) - if contact_link is not None: - contacts = await uow.get_for(Contact).find_all_generic( - parent_ref_id=contact_domain.ref_id, - allow_archived=False, - ref_id=contact_link.contacts_ref_ids, - ) - else: - contacts = [] - - return MetricEntryLoadResult( - metric_entry=metric_entry, - tags=tags, - contacts=contacts, - note=note, + metric_entry, + allow_archived=allow_archived, ) diff --git a/src/core/jupiter/core/metrics/sub/entry/use_case/load_public.py b/src/core/jupiter/core/metrics/sub/entry/use_case/load_public.py new file mode 100644 index 000000000..1e8fdbc43 --- /dev/null +++ b/src/core/jupiter/core/metrics/sub/entry/use_case/load_public.py @@ -0,0 +1,85 @@ +"""Guest readonly use case for loading a published metric entry.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.metrics.collection import MetricCollection +from jupiter.core.metrics.root import Metric +from jupiter.core.metrics.sub.entry.root import MetricEntry +from jupiter.core.metrics.sub.entry.service.load import ( + MetricEntryLoadResult, + MetricEntryLoadService, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class MetricEntryLoadPublicArgs(UseCaseArgsBase): + """MetricEntryLoadPublic args.""" + + external_id: PublishExternalId + + +class MetricEntryLoadPublicUseCase( + JupiterGuestReadonlyUseCase[MetricEntryLoadPublicArgs, MetricEntryLoadResult] +): + """Load a published metric entry by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: MetricEntryLoadPublicArgs, + ) -> MetricEntryLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.METRIC_ENTRY.value: + raise InputValidationError( + "The publish entity does not refer to a metric entry." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + metric_collection = await uow.get_for(MetricCollection).load_by_parent( + publish_domain.workspace.ref_id + ) + metric_entry = await uow.get_for(MetricEntry).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + metric = await uow.get_for(Metric).load_by_id( + metric_entry.metric.ref_id, + allow_archived=False, + ) + if metric.parent_ref_id != metric_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace metric entry." + ) + + return await MetricEntryLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + metric_entry, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/schedule/sub/event_full_days/component/editor.tsx b/src/core/jupiter/core/schedule/sub/event_full_days/component/editor.tsx new file mode 100644 index 000000000..a19f172a4 --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/event_full_days/component/editor.tsx @@ -0,0 +1,233 @@ +import type { + Contact, + ScheduleEventFullDays, + ScheduleStreamSummary, + Tag, + TimeEventFullDaysBlock, +} from "@jupiter/webapi-client"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import { + Button, + ButtonGroup, + FormControl, + InputLabel, + OutlinedInput, + Stack, +} from "@mui/material"; +import { entityLinkStd } from "#/core/common/entity-link"; +import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; +import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; +import type { ActionResult } from "#/core/infra/action-result"; +import { FieldError } from "#/core/infra/component/errors"; +import { + ActionMultipleSpread, + ActionSingle, + SectionActions, +} from "#/core/infra/component/section-actions"; +import { SectionCard } from "#/core/infra/component/section-card"; +import { ScheduleStreamSelect } from "#/core/schedule/component/select"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import type { TopLevelInfo } from "#/core/infra/top-level-context"; + +interface ScheduleEventFullDaysEditorProps { + scheduleEventFullDays: ScheduleEventFullDays; + timeEventFullDaysBlock: TimeEventFullDaysBlock; + allScheduleStreams: Array<ScheduleStreamSummary>; + tags: Array<Tag>; + contacts: Array<Contact>; + allTags: Array<Tag>; + allContacts: Array<Contact>; + inputsEnabled: boolean; + corePropertyEditable: boolean; + topLevelInfo: TopLevelInfo; + actionResult?: ActionResult<unknown>; + durationDays?: number; + onDurationDaysChange?: (value: number) => void; +} + +export function ScheduleEventFullDaysEditor( + props: ScheduleEventFullDaysEditorProps, +) { + const isBigScreen = useBigScreen(); + const { + scheduleEventFullDays, + timeEventFullDaysBlock, + allScheduleStreams, + tags, + contacts, + allTags, + allContacts, + } = props; + const editable = props.inputsEnabled && props.corePropertyEditable; + const durationDays = + props.durationDays ?? timeEventFullDaysBlock.duration_days; + + const allScheduleStreamsByRefId = new Map( + allScheduleStreams.map((st) => [st.ref_id, st]), + ); + const selectedScheduleStream = allScheduleStreamsByRefId.get( + scheduleEventFullDays.schedule_stream_ref_id, + ); + + return ( + <SectionCard + id="schedule-event-full-days-properties" + title="Properties" + actions={ + props.inputsEnabled ? ( + <SectionActions + id="schedule-event-full-days-properties" + topLevelInfo={props.topLevelInfo} + inputsEnabled={props.inputsEnabled} + actions={[ + ActionMultipleSpread({ + actions: [ + ActionSingle({ + text: "Save", + value: "update", + highlight: true, + disabled: !props.corePropertyEditable, + }), + ActionSingle({ + text: "Change Stream", + value: "change-schedule-stream", + disabled: !props.corePropertyEditable, + }), + ], + }), + ]} + /> + ) : undefined + } + > + <FormControl fullWidth> + <InputLabel id="scheduleStreamRefId">Schedule Stream</InputLabel> + {selectedScheduleStream ? ( + <ScheduleStreamSelect + labelId="scheduleStreamRefId" + label="Schedule Stream" + name="scheduleStreamRefId" + readOnly={!editable} + allScheduleStreams={allScheduleStreams} + defaultValue={selectedScheduleStream} + /> + ) : ( + <OutlinedInput + label="Schedule Stream" + readOnly + disabled + value="—" + /> + )} + <FieldError + actionResult={props.actionResult} + fieldName="/schedule_stream_ref_id" + /> + </FormControl> + + <Stack direction={isBigScreen ? "row" : "column"} spacing={2} useFlexGap> + <FormControl fullWidth={!isBigScreen} sx={{ flexGrow: 1 }}> + <InputLabel id="name">Name</InputLabel> + <OutlinedInput + label="name" + name="name" + readOnly={!editable} + defaultValue={scheduleEventFullDays.name} + /> + <FieldError actionResult={props.actionResult} fieldName="/name" /> + </FormControl> + </Stack> + + <Stack direction={isBigScreen ? "row" : "column"} useFlexGap gap={2}> + <FormControl fullWidth sx={{ flexGrow: 1 }}> + <TagsEditor + name="tags_names" + allTags={allTags} + defaultValue={tags.map((t) => t.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd( + NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS, + scheduleEventFullDays.ref_id, + )} + aloneOnLine={!isBigScreen} + /> + </FormControl> + + <FormControl fullWidth sx={{ flexGrow: 1 }}> + <ContactsEditor + name="contacts_names" + allContacts={allContacts} + defaultValue={contacts.map((contact) => contact.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd( + NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS, + scheduleEventFullDays.ref_id, + )} + aloneOnLine={!isBigScreen} + /> + </FormControl> + </Stack> + + <FormControl fullWidth> + <InputLabel id="startDate" shrink margin="dense"> + Start Date + </InputLabel> + <OutlinedInput + type="date" + notched + label="startDate" + name="startDate" + readOnly={!editable} + disabled={!editable} + defaultValue={timeEventFullDaysBlock.start_date} + /> + + <FieldError actionResult={props.actionResult} fieldName="/start_date" /> + </FormControl> + + <Stack spacing={2} direction="row"> + <ButtonGroup variant="outlined" disabled={!editable}> + <Button + disabled={!editable} + variant={durationDays === 1 ? "contained" : "outlined"} + onClick={() => props.onDurationDaysChange?.(1)} + > + 1D + </Button> + <Button + disabled={!editable} + variant={durationDays === 3 ? "contained" : "outlined"} + onClick={() => props.onDurationDaysChange?.(3)} + > + 3d + </Button> + <Button + disabled={!editable} + variant={durationDays === 7 ? "contained" : "outlined"} + onClick={() => props.onDurationDaysChange?.(7)} + > + 7d + </Button> + </ButtonGroup> + + <FormControl fullWidth> + <InputLabel id="durationDays" shrink margin="dense"> + Duration (Days) + </InputLabel> + <OutlinedInput + type="number" + label="Duration (Days)" + name="durationDays" + readOnly={!editable} + value={durationDays} + onChange={(e) => + props.onDurationDaysChange?.(parseInt(e.target.value, 10)) + } + /> + + <FieldError actionResult={props.actionResult} fieldName="/duration_days" /> + </FormControl> + </Stack> + </SectionCard> + ); +} diff --git a/src/core/jupiter/core/schedule/sub/event_full_days/root.py b/src/core/jupiter/core/schedule/sub/event_full_days/root.py index 12042ce56..e27c8cd56 100644 --- a/src/core/jupiter/core/schedule/sub/event_full_days/root.py +++ b/src/core/jupiter/core/schedule/sub/event_full_days/root.py @@ -1,6 +1,7 @@ """A full day block in a schedule.""" from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( TimeEventFullDaysBlock, @@ -49,6 +50,10 @@ class ScheduleEventFullDays(LeafEntity): Note, owner=IsEntityLinkStd(NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value), ) + publish_entity = OwnsAtMostOne( + PublishEntity, + owner=IsEntityLinkStd(NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value), + ) @staticmethod @create_entity_action diff --git a/src/core/jupiter/core/schedule/sub/event_full_days/service/__init__.py b/src/core/jupiter/core/schedule/sub/event_full_days/service/__init__.py new file mode 100644 index 000000000..121eca1bd --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/event_full_days/service/__init__.py @@ -0,0 +1 @@ +"""Services for schedule full days events.""" diff --git a/src/core/jupiter/core/schedule/sub/event_full_days/service/load.py b/src/core/jupiter/core/schedule/sub/event_full_days/service/load.py new file mode 100644 index 000000000..30088d272 --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/event_full_days/service/load.py @@ -0,0 +1,128 @@ +"""Shared service for loading a schedule full days event.""" + +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( + TimeEventFullDaysBlock, +) +from jupiter.core.application.fast_info_repository import ScheduleStreamSummary +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.schedule.sub.event_full_days.root import ScheduleEventFullDays +from jupiter.core.schedule.sub.stream.root import ScheduleStream +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result +from jupiter.framework.utils.generic_loader import generic_loader + + +@use_case_result +class ScheduleEventFullDaysLoadResult(UseCaseResultBase): + """ScheduleEventFullDaysLoadResult.""" + + schedule_event_full_days: ScheduleEventFullDays + time_event_full_days_block: TimeEventFullDaysBlock + note: Note | None + tags: list[Tag] + contacts: list[Contact] + schedule_stream: ScheduleStreamSummary + publish_entity: PublishEntity | None + + +class ScheduleEventFullDaysLoadService: + """Shared service for loading a schedule full days event.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + schedule_event_full_days: ScheduleEventFullDays, + *, + allow_archived: bool = False, + include_publish_entity: bool = True, + ) -> ScheduleEventFullDaysLoadResult: + """Load a schedule full days event and its dependent entities.""" + ( + schedule_event_full_days, + time_event_full_days_block, + note, + ) = await generic_loader( + uow, + ScheduleEventFullDays, + schedule_event_full_days.ref_id, + ScheduleEventFullDays.time_event_full_days_block, + ScheduleEventFullDays.note, + allow_archived=allow_archived, + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std( + NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, + schedule_event_full_days.ref_id, + ), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace_ref_id, + ) + contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( + EntityLink.std( + NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, + schedule_event_full_days.ref_id, + ), + ) + if contact_link is not None: + contacts = await uow.get_for(Contact).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + ref_id=contact_link.contacts_ref_ids, + ) + else: + contacts = [] + + schedule_stream = await uow.get_for(ScheduleStream).load_by_id( + schedule_event_full_days.schedule_stream_ref_id, + ) + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std( + NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, + schedule_event_full_days.ref_id, + ), + allow_archived=allow_archived, + ) + + return ScheduleEventFullDaysLoadResult( + schedule_event_full_days=schedule_event_full_days, + time_event_full_days_block=time_event_full_days_block, + note=note, + tags=tags, + contacts=contacts, + schedule_stream=ScheduleStreamSummary( + ref_id=schedule_stream.ref_id, + source=schedule_stream.source, + name=schedule_stream.name, + color=schedule_stream.color, + ), + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/schedule/sub/event_full_days/use_case/load.py b/src/core/jupiter/core/schedule/sub/event_full_days/use_case/load.py index 708b13bab..f7f822f8c 100644 --- a/src/core/jupiter/core/schedule/sub/event_full_days/use_case/load.py +++ b/src/core/jupiter/core/schedule/sub/event_full_days/use_case/load.py @@ -1,36 +1,25 @@ """Use case for loading a schedule full days event.""" -from jupiter.core.common.sub.contacts.root import ContactDomain -from jupiter.core.common.sub.contacts.sub.contact.root import Contact -from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository -from jupiter.core.common.sub.notes.root import Note -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository -from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( - TimeEventFullDaysBlock, -) from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.named_entity_tag import NamedEntityTag -from jupiter.core.schedule.sub.event_full_days.root import ( - ScheduleEventFullDays, +from jupiter.core.schedule.sub.event_full_days.root import ScheduleEventFullDays +from jupiter.core.schedule.sub.event_full_days.service.load import ( + ScheduleEventFullDaysLoadResult, + ScheduleEventFullDaysLoadService, ) from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.storage.repository import DomainUnitOfWork -from jupiter.framework.use_case import ( - readonly_use_case, -) -from jupiter.framework.use_case_io import ( - UseCaseArgsBase, - UseCaseResultBase, - use_case_args, - use_case_result, -) -from jupiter.framework.utils.generic_loader import generic_loader +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + +__all__ = [ + "ScheduleEventFullDaysLoadArgs", + "ScheduleEventFullDaysLoadResult", + "ScheduleEventFullDaysLoadUseCase", +] @use_case_args @@ -41,17 +30,6 @@ class ScheduleEventFullDaysLoadArgs(UseCaseArgsBase): allow_archived: bool | None -@use_case_result -class ScheduleEventFullDaysLoadResult(UseCaseResultBase): - """Result.""" - - schedule_event_full_days: ScheduleEventFullDays - time_event_full_days_block: TimeEventFullDaysBlock - note: Note | None - tags: list[Tag] - contacts: list[Contact] - - @readonly_use_case(WorkspaceFeature.SCHEDULE) class ScheduleEventFullDaysLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[ @@ -68,55 +46,14 @@ async def _perform_transactional_read( ) -> ScheduleEventFullDaysLoadResult: """Execute the command's action.""" allow_archived = args.allow_archived or False - ( - schedule_event_full_days, - time_event_full_days_block, - note, - ) = await generic_loader( - uow, - ScheduleEventFullDays, - args.ref_id, - ScheduleEventFullDays.time_event_full_days_block, - ScheduleEventFullDays.note, - allow_archived=allow_archived, - ) - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std( - NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, - schedule_event_full_days.ref_id, - ), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - contact_domain = await uow.get_for(ContactDomain).load_by_parent( - context.workspace.ref_id, - ) - contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( - EntityLink.std( - NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, - schedule_event_full_days.ref_id, - ), + schedule_event_full_days = await uow.get_for(ScheduleEventFullDays).load_by_id( + args.ref_id, allow_archived=allow_archived ) - if contact_link is not None: - contacts = await uow.get_for(Contact).find_all_generic( - parent_ref_id=contact_domain.ref_id, - allow_archived=False, - ref_id=contact_link.contacts_ref_ids, - ) - else: - contacts = [] - return ScheduleEventFullDaysLoadResult( - schedule_event_full_days=schedule_event_full_days, - time_event_full_days_block=time_event_full_days_block, - note=note, - tags=tags, - contacts=contacts, + return await ScheduleEventFullDaysLoadService().do_it( + uow, + context.workspace.ref_id, + schedule_event_full_days, + allow_archived=allow_archived, ) diff --git a/src/core/jupiter/core/schedule/sub/event_full_days/use_case/load_public.py b/src/core/jupiter/core/schedule/sub/event_full_days/use_case/load_public.py new file mode 100644 index 000000000..7e4456819 --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/event_full_days/use_case/load_public.py @@ -0,0 +1,87 @@ +"""Guest readonly use case for loading a published schedule full days event.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.schedule.domain import ScheduleDomain +from jupiter.core.schedule.sub.event_full_days.root import ScheduleEventFullDays +from jupiter.core.schedule.sub.event_full_days.service.load import ( + ScheduleEventFullDaysLoadResult, + ScheduleEventFullDaysLoadService, +) +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class ScheduleEventFullDaysLoadPublicArgs(UseCaseArgsBase): + """ScheduleEventFullDaysLoadPublic args.""" + + external_id: PublishExternalId + + +class ScheduleEventFullDaysLoadPublicUseCase( + JupiterGuestReadonlyUseCase[ + ScheduleEventFullDaysLoadPublicArgs, ScheduleEventFullDaysLoadResult + ] +): + """Load a published schedule full days event by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: ScheduleEventFullDaysLoadPublicArgs, + ) -> ScheduleEventFullDaysLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if ( + publish_entity.owner.the_type + != NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value + ): + raise InputValidationError( + "The publish entity does not refer to a schedule full days event." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + schedule_domain = await uow.get_for(ScheduleDomain).load_by_parent( + publish_domain.workspace.ref_id + ) + schedule_event_full_days = await uow.get_for( + ScheduleEventFullDays + ).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if schedule_event_full_days.parent_ref_id != schedule_domain.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace schedule event." + ) + + return await ScheduleEventFullDaysLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + schedule_event_full_days, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/schedule/sub/event_in_day/component/editor.tsx b/src/core/jupiter/core/schedule/sub/event_in_day/component/editor.tsx new file mode 100644 index 000000000..8b99476e4 --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/event_in_day/component/editor.tsx @@ -0,0 +1,264 @@ +import type { + Contact, + ScheduleEventInDay, + ScheduleStreamSummary, + Tag, + TimeEventInDayBlock, +} from "@jupiter/webapi-client"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import { + Button, + ButtonGroup, + FormControl, + InputLabel, + OutlinedInput, + Stack, +} from "@mui/material"; +import { entityLinkStd } from "#/core/common/entity-link"; +import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; +import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; +import type { ActionResult } from "#/core/infra/action-result"; +import { FieldError } from "#/core/infra/component/errors"; +import { + ActionMultipleSpread, + ActionSingle, + SectionActions, +} from "#/core/infra/component/section-actions"; +import { SectionCard } from "#/core/infra/component/section-card"; +import { ScheduleStreamSelect } from "#/core/schedule/component/select"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import type { TopLevelInfo } from "#/core/infra/top-level-context"; + +interface ScheduleEventInDayEditorProps { + scheduleEventInDay: ScheduleEventInDay; + timeEventInDayBlock: TimeEventInDayBlock; + allScheduleStreams: Array<ScheduleStreamSummary>; + tags: Array<Tag>; + contacts: Array<Contact>; + allTags: Array<Tag>; + allContacts: Array<Contact>; + inputsEnabled: boolean; + corePropertyEditable: boolean; + topLevelInfo: TopLevelInfo; + actionResult?: ActionResult<unknown>; + startDate: string; + startTimeInDay: string; + durationMins: number; + onStartDateChange?: (value: string) => void; + onStartTimeInDayChange?: (value: string) => void; + onDurationMinsChange?: (value: number) => void; +} + +export function ScheduleEventInDayEditor(props: ScheduleEventInDayEditorProps) { + const isBigScreen = useBigScreen(); + const { + scheduleEventInDay, + tags, + contacts, + allTags, + allContacts, + allScheduleStreams, + startDate, + startTimeInDay, + durationMins, + } = props; + const editable = props.inputsEnabled && props.corePropertyEditable; + + const allScheduleStreamsByRefId = new Map( + allScheduleStreams.map((st) => [st.ref_id, st]), + ); + const selectedScheduleStream = allScheduleStreamsByRefId.get( + scheduleEventInDay.schedule_stream_ref_id, + ); + + return ( + <SectionCard + id="schedule-event-in-day-properties" + title="Properties" + actions={ + props.inputsEnabled ? ( + <SectionActions + id="schedule-event-in-day-properties" + topLevelInfo={props.topLevelInfo} + inputsEnabled={props.inputsEnabled} + actions={[ + ActionMultipleSpread({ + actions: [ + ActionSingle({ + text: "Save", + value: "update", + highlight: true, + disabled: !props.corePropertyEditable, + }), + ActionSingle({ + text: "Change Stream", + value: "change-schedule-stream", + disabled: !props.corePropertyEditable, + }), + ], + }), + ]} + /> + ) : undefined + } + > + <input + type="hidden" + name="userTimezone" + value={props.topLevelInfo.user.timezone} + /> + <FormControl fullWidth> + <InputLabel id="scheduleStreamRefId">Schedule Stream</InputLabel> + {selectedScheduleStream ? ( + <ScheduleStreamSelect + labelId="scheduleStreamRefId" + label="Schedule Stream" + name="scheduleStreamRefId" + readOnly={!editable} + allScheduleStreams={allScheduleStreams} + defaultValue={selectedScheduleStream} + /> + ) : ( + <OutlinedInput + label="Schedule Stream" + readOnly + disabled + value="—" + /> + )} + <FieldError + actionResult={props.actionResult} + fieldName="/schedule_stream_ref_id" + /> + </FormControl> + + <FormControl fullWidth={!isBigScreen} sx={{ flexGrow: 1 }}> + <InputLabel id="name">Name</InputLabel> + <OutlinedInput + label="name" + name="name" + readOnly={!editable} + defaultValue={scheduleEventInDay.name} + /> + <FieldError actionResult={props.actionResult} fieldName="/name" /> + </FormControl> + + <Stack direction={isBigScreen ? "row" : "column"} useFlexGap gap={2}> + <FormControl fullWidth sx={{ flexGrow: 1 }}> + <TagsEditor + name="tags_names" + allTags={allTags} + defaultValue={tags.map((t) => t.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd( + NamedEntityTag.SCHEDULE_EVENT_IN_DAY, + scheduleEventInDay.ref_id, + )} + aloneOnLine={!isBigScreen} + /> + </FormControl> + + <FormControl fullWidth sx={{ flexGrow: 1 }}> + <ContactsEditor + name="contacts_names" + allContacts={allContacts} + defaultValue={contacts.map((contact) => contact.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd( + NamedEntityTag.SCHEDULE_EVENT_IN_DAY, + scheduleEventInDay.ref_id, + )} + aloneOnLine={!isBigScreen} + /> + </FormControl> + </Stack> + + <Stack direction="row" useFlexGap gap={2}> + <FormControl fullWidth> + <InputLabel id="startDate" shrink margin="dense"> + Start Date + </InputLabel> + <OutlinedInput + type="date" + notched + label="startDate" + name="startDate" + readOnly={!editable} + disabled={!editable} + value={startDate} + onChange={(e) => props.onStartDateChange?.(e.target.value)} + /> + + <FieldError actionResult={props.actionResult} fieldName="/start_date" /> + </FormControl> + + <FormControl fullWidth> + <InputLabel id="startTimeInDay" shrink margin="dense"> + Start Time + </InputLabel> + <OutlinedInput + type="time" + label="startTimeInDay" + name="startTimeInDay" + readOnly={!editable} + value={startTimeInDay} + onChange={(e) => props.onStartTimeInDayChange?.(e.target.value)} + /> + + <FieldError + actionResult={props.actionResult} + fieldName="/start_time_in_day" + /> + </FormControl> + </Stack> + + <Stack spacing={2} direction="row"> + <ButtonGroup variant="outlined" disabled={!editable}> + <Button + disabled={!editable} + variant={durationMins === 15 ? "contained" : "outlined"} + onClick={() => props.onDurationMinsChange?.(15)} + > + 15m + </Button> + <Button + disabled={!editable} + variant={durationMins === 30 ? "contained" : "outlined"} + onClick={() => props.onDurationMinsChange?.(30)} + > + 30m + </Button> + <Button + disabled={!editable} + variant={durationMins === 60 ? "contained" : "outlined"} + onClick={() => props.onDurationMinsChange?.(60)} + > + 60m + </Button> + </ButtonGroup> + + <FormControl fullWidth> + <InputLabel id="durationMins" shrink margin="dense"> + Duration (Mins) + </InputLabel> + <OutlinedInput + type="number" + label="Duration (Mins)" + name="durationMins" + readOnly={!editable} + value={durationMins} + onChange={(e) => { + if (Number.isNaN(parseInt(e.target.value, 10))) { + props.onDurationMinsChange?.(0); + return; + } + props.onDurationMinsChange?.(parseInt(e.target.value, 10)); + }} + /> + + <FieldError actionResult={props.actionResult} fieldName="/duration_mins" /> + </FormControl> + </Stack> + </SectionCard> + ); +} diff --git a/src/core/jupiter/core/schedule/sub/event_in_day/root.py b/src/core/jupiter/core/schedule/sub/event_in_day/root.py index 38a96c7a1..1953a3c60 100644 --- a/src/core/jupiter/core/schedule/sub/event_in_day/root.py +++ b/src/core/jupiter/core/schedule/sub/event_in_day/root.py @@ -1,6 +1,7 @@ """An event in a schedule.""" from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( TimeEventInDayBlock, @@ -49,6 +50,9 @@ class ScheduleEventInDay(LeafEntity): Note, owner=IsEntityLinkStd(NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value), ) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value) + ) @staticmethod @create_entity_action diff --git a/src/core/jupiter/core/schedule/sub/event_in_day/service/__init__.py b/src/core/jupiter/core/schedule/sub/event_in_day/service/__init__.py new file mode 100644 index 000000000..b964c96d7 --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/event_in_day/service/__init__.py @@ -0,0 +1 @@ +"""Services for schedule events in day.""" diff --git a/src/core/jupiter/core/schedule/sub/event_in_day/service/load.py b/src/core/jupiter/core/schedule/sub/event_in_day/service/load.py new file mode 100644 index 000000000..121793373 --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/event_in_day/service/load.py @@ -0,0 +1,123 @@ +"""Shared service for loading a schedule event in day.""" + +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( + TimeEventInDayBlock, +) +from jupiter.core.application.fast_info_repository import ScheduleStreamSummary +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.schedule.sub.event_in_day.root import ScheduleEventInDay +from jupiter.core.schedule.sub.stream.root import ScheduleStream +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result +from jupiter.framework.utils.generic_loader import generic_loader + + +@use_case_result +class ScheduleEventInDayLoadResult(UseCaseResultBase): + """ScheduleEventInDayLoadResult.""" + + schedule_event_in_day: ScheduleEventInDay + time_event_in_day_block: TimeEventInDayBlock + note: Note | None + tags: list[Tag] + contacts: list[Contact] + schedule_stream: ScheduleStreamSummary + publish_entity: PublishEntity | None + + +class ScheduleEventInDayLoadService: + """Shared service for loading a schedule event in day.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + schedule_event_in_day: ScheduleEventInDay, + *, + allow_archived: bool = False, + include_publish_entity: bool = True, + ) -> ScheduleEventInDayLoadResult: + """Load a schedule event in day and its dependent entities.""" + schedule_event_in_day, time_event_in_day_block, note = await generic_loader( + uow, + ScheduleEventInDay, + schedule_event_in_day.ref_id, + ScheduleEventInDay.time_event_in_day_block, + ScheduleEventInDay.note, + allow_archived=allow_archived, + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std( + NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, schedule_event_in_day.ref_id + ), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace_ref_id, + ) + contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( + EntityLink.std( + NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, + schedule_event_in_day.ref_id, + ), + ) + if contact_link is not None: + contacts = await uow.get_for(Contact).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + ref_id=contact_link.contacts_ref_ids, + ) + else: + contacts = [] + + schedule_stream = await uow.get_for(ScheduleStream).load_by_id( + schedule_event_in_day.schedule_stream_ref_id, + ) + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std( + NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, + schedule_event_in_day.ref_id, + ), + allow_archived=allow_archived, + ) + + return ScheduleEventInDayLoadResult( + schedule_event_in_day=schedule_event_in_day, + time_event_in_day_block=time_event_in_day_block, + note=note, + tags=tags, + contacts=contacts, + schedule_stream=ScheduleStreamSummary( + ref_id=schedule_stream.ref_id, + source=schedule_stream.source, + name=schedule_stream.name, + color=schedule_stream.color, + ), + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load.py b/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load.py index 118194145..cd5e38f10 100644 --- a/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load.py +++ b/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load.py @@ -1,36 +1,25 @@ """Use case for loading a schedule in day event.""" -from jupiter.core.common.sub.contacts.root import ContactDomain -from jupiter.core.common.sub.contacts.sub.contact.root import Contact -from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository -from jupiter.core.common.sub.notes.root import Note -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository -from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( - TimeEventInDayBlock, -) from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.named_entity_tag import NamedEntityTag -from jupiter.core.schedule.sub.event_in_day.root import ( - ScheduleEventInDay, +from jupiter.core.schedule.sub.event_in_day.root import ScheduleEventInDay +from jupiter.core.schedule.sub.event_in_day.service.load import ( + ScheduleEventInDayLoadResult, + ScheduleEventInDayLoadService, ) from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.storage.repository import DomainUnitOfWork -from jupiter.framework.use_case import ( - readonly_use_case, -) -from jupiter.framework.use_case_io import ( - UseCaseArgsBase, - UseCaseResultBase, - use_case_args, - use_case_result, -) -from jupiter.framework.utils.generic_loader import generic_loader +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + +__all__ = [ + "ScheduleEventInDayLoadArgs", + "ScheduleEventInDayLoadResult", + "ScheduleEventInDayLoadUseCase", +] @use_case_args @@ -41,17 +30,6 @@ class ScheduleEventInDayLoadArgs(UseCaseArgsBase): allow_archived: bool | None -@use_case_result -class ScheduleEventInDayLoadResult(UseCaseResultBase): - """Result.""" - - schedule_event_in_day: ScheduleEventInDay - time_event_in_day_block: TimeEventInDayBlock - note: Note | None - tags: list[Tag] - contacts: list[Contact] - - @readonly_use_case(WorkspaceFeature.SCHEDULE) class ScheduleEventInDayLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[ @@ -68,50 +46,14 @@ async def _perform_transactional_read( ) -> ScheduleEventInDayLoadResult: """Execute the command's action.""" allow_archived = args.allow_archived or False - schedule_event_in_day, time_event_in_day_block, note = await generic_loader( - uow, - ScheduleEventInDay, - args.ref_id, - ScheduleEventInDay.time_event_in_day_block, - ScheduleEventInDay.note, - allow_archived=allow_archived, - ) - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std( - NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, schedule_event_in_day.ref_id - ), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - contact_domain = await uow.get_for(ContactDomain).load_by_parent( - context.workspace.ref_id, + schedule_event_in_day = await uow.get_for(ScheduleEventInDay).load_by_id( + args.ref_id, allow_archived=allow_archived ) - contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( - EntityLink.std( - NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, - schedule_event_in_day.ref_id, - ), - ) - if contact_link is not None: - contacts = await uow.get_for(Contact).find_all_generic( - parent_ref_id=contact_domain.ref_id, - allow_archived=False, - ref_id=contact_link.contacts_ref_ids, - ) - else: - contacts = [] - return ScheduleEventInDayLoadResult( - schedule_event_in_day=schedule_event_in_day, - time_event_in_day_block=time_event_in_day_block, - note=note, - tags=tags, - contacts=contacts, + return await ScheduleEventInDayLoadService().do_it( + uow, + context.workspace.ref_id, + schedule_event_in_day, + allow_archived=allow_archived, ) diff --git a/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public.py b/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public.py new file mode 100644 index 000000000..12f144a45 --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public.py @@ -0,0 +1,82 @@ +"""Guest readonly use case for loading a published schedule event in day.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.schedule.domain import ScheduleDomain +from jupiter.core.schedule.sub.event_in_day.root import ScheduleEventInDay +from jupiter.core.schedule.sub.event_in_day.service.load import ( + ScheduleEventInDayLoadResult, + ScheduleEventInDayLoadService, +) +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class ScheduleEventInDayLoadPublicArgs(UseCaseArgsBase): + """ScheduleEventInDayLoadPublic args.""" + + external_id: PublishExternalId + + +class ScheduleEventInDayLoadPublicUseCase( + JupiterGuestReadonlyUseCase[ + ScheduleEventInDayLoadPublicArgs, ScheduleEventInDayLoadResult + ] +): + """Load a published schedule event in day by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: ScheduleEventInDayLoadPublicArgs, + ) -> ScheduleEventInDayLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value: + raise InputValidationError( + "The publish entity does not refer to a schedule event in day." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + schedule_domain = await uow.get_for(ScheduleDomain).load_by_parent( + publish_domain.workspace.ref_id + ) + schedule_event_in_day = await uow.get_for(ScheduleEventInDay).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if schedule_event_in_day.parent_ref_id != schedule_domain.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace schedule event." + ) + + return await ScheduleEventInDayLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + schedule_event_in_day, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/smart_lists/sub/item/component/editor.tsx b/src/core/jupiter/core/smart_lists/sub/item/component/editor.tsx new file mode 100644 index 000000000..aa1424f13 --- /dev/null +++ b/src/core/jupiter/core/smart_lists/sub/item/component/editor.tsx @@ -0,0 +1,127 @@ +import type { Contact, SmartListItem, Tag } from "@jupiter/webapi-client"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import { + FormControl, + FormControlLabel, + InputLabel, + OutlinedInput, + Stack, + Switch, +} from "@mui/material"; +import { entityLinkStd } from "#/core/common/entity-link"; +import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; +import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; +import type { ActionResult } from "#/core/infra/action-result"; +import { FieldError } from "#/core/infra/component/errors"; +import { + ActionSingle, + SectionActions, +} from "#/core/infra/component/section-actions"; +import { SectionCard } from "#/core/infra/component/section-card"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import type { TopLevelInfo } from "#/core/infra/top-level-context"; + +interface SmartListItemEditorProps { + item: SmartListItem; + genericTags: Array<Tag>; + contacts: Array<Contact>; + allTags: Array<Tag>; + allContacts: Array<Contact>; + inputsEnabled: boolean; + topLevelInfo: TopLevelInfo; + actionResult?: ActionResult<unknown>; +} + +export function SmartListItemEditor(props: SmartListItemEditorProps) { + const isBigScreen = useBigScreen(); + const { item, genericTags, contacts, allTags, allContacts } = props; + + return ( + <SectionCard + title="Properties" + actions={ + props.inputsEnabled ? ( + <SectionActions + id="smart-list-item-properties" + topLevelInfo={props.topLevelInfo} + inputsEnabled={props.inputsEnabled} + actions={[ + ActionSingle({ + id: "smart-list-item-update", + text: "Save", + value: "update", + highlight: true, + }), + ]} + /> + ) : undefined + } + > + <Stack direction="row" useFlexGap spacing={1}> + <FormControl fullWidth sx={{ flexGrow: 1 }}> + <InputLabel id="name">Name</InputLabel> + <OutlinedInput + label="Name" + defaultValue={item.name} + name="name" + readOnly={!props.inputsEnabled} + /> + + <FieldError actionResult={props.actionResult} fieldName="/name" /> + </FormControl> + </Stack> + + <Stack direction={isBigScreen ? "row" : "column"} spacing={2}> + <FormControl sx={{ flexGrow: 1, minWidth: "25%" }}> + <TagsEditor + name="generic_tags_names" + aloneOnLine + allTags={allTags} + defaultValue={genericTags.map((t) => t.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.SMART_LIST_ITEM, item.ref_id)} + label="Tags" + /> + </FormControl> + + <FormControl sx={{ flexGrow: 1, minWidth: "25%" }}> + <ContactsEditor + name="contacts_names" + aloneOnLine + allContacts={allContacts} + defaultValue={contacts.map((c) => c.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.SMART_LIST_ITEM, item.ref_id)} + label="Contacts" + /> + </FormControl> + </Stack> + + <FormControl fullWidth> + <FormControlLabel + control={ + <Switch + name="isDone" + readOnly={!props.inputsEnabled} + disabled={!props.inputsEnabled} + defaultChecked={item.is_done} + /> + } + label="Is Done" + /> + <FieldError actionResult={props.actionResult} fieldName="/is_done" /> + </FormControl> + + <FormControl fullWidth> + <InputLabel id="url">Url [Optional]</InputLabel> + <OutlinedInput + label="Url" + name="url" + readOnly={!props.inputsEnabled} + defaultValue={item.url} + /> + <FieldError actionResult={props.actionResult} fieldName="/url" /> + </FormControl> + </SectionCard> + ); +} diff --git a/src/core/jupiter/core/smart_lists/sub/item/root.py b/src/core/jupiter/core/smart_lists/sub/item/root.py index 76c889e49..cdf33a6ec 100644 --- a/src/core/jupiter/core/smart_lists/sub/item/root.py +++ b/src/core/jupiter/core/smart_lists/sub/item/root.py @@ -1,6 +1,7 @@ """A smart list item.""" from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.url import URL from jupiter.core.named_entity_tag import NamedEntityTag @@ -37,6 +38,9 @@ class SmartListItem(LeafEntity): note = OwnsAtMostOne( Note, owner=IsEntityLinkStd(NamedEntityTag.SMART_LIST_ITEM.value) ) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.SMART_LIST_ITEM.value) + ) @staticmethod @create_entity_action diff --git a/src/core/jupiter/core/smart_lists/sub/item/service/__init__.py b/src/core/jupiter/core/smart_lists/sub/item/service/__init__.py new file mode 100644 index 000000000..d9b040fbf --- /dev/null +++ b/src/core/jupiter/core/smart_lists/sub/item/service/__init__.py @@ -0,0 +1 @@ +"""Services for smart list items.""" diff --git a/src/core/jupiter/core/smart_lists/sub/item/service/load.py b/src/core/jupiter/core/smart_lists/sub/item/service/load.py new file mode 100644 index 000000000..002c584e0 --- /dev/null +++ b/src/core/jupiter/core/smart_lists/sub/item/service/load.py @@ -0,0 +1,97 @@ +"""Shared service for loading a smart list item.""" + +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.smart_lists.sub.item.root import SmartListItem +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result +from jupiter.framework.utils.generic_loader import generic_loader + + +@use_case_result +class SmartListItemLoadResult(UseCaseResultBase): + """SmartListItemLoadResult.""" + + item: SmartListItem + generic_tags: list[Tag] + contacts: list[Contact] + note: Note | None + publish_entity: PublishEntity | None + + +class SmartListItemLoadService: + """Shared service for loading a smart list item.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + item: SmartListItem, + *, + allow_archived: bool = False, + include_publish_entity: bool = True, + ) -> SmartListItemLoadResult: + """Load a smart list item and its dependent entities.""" + item, note = await generic_loader( + uow, + SmartListItem, + item.ref_id, + SmartListItem.note, + allow_archived=allow_archived, + allow_subentity_archived=allow_archived, + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std(NamedEntityTag.SMART_LIST_ITEM.value, item.ref_id), + ) + if tag_link is not None: + generic_tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + generic_tags = [] + + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace_ref_id, + ) + contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.SMART_LIST_ITEM.value, item.ref_id), + ) + if contact_link is not None: + contacts = await uow.get_for(Contact).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + ref_id=contact_link.contacts_ref_ids, + ) + else: + contacts = [] + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std(NamedEntityTag.SMART_LIST_ITEM.value, item.ref_id), + allow_archived=allow_archived, + ) + + return SmartListItemLoadResult( + item=item, + generic_tags=generic_tags, + contacts=contacts, + note=note, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/smart_lists/sub/item/use_case/load.py b/src/core/jupiter/core/smart_lists/sub/item/use_case/load.py index 8d8704b01..c303bba56 100644 --- a/src/core/jupiter/core/smart_lists/sub/item/use_case/load.py +++ b/src/core/jupiter/core/smart_lists/sub/item/use_case/load.py @@ -1,31 +1,25 @@ """Use case for loading a smart list item.""" -from jupiter.core.common.sub.contacts.root import ContactDomain -from jupiter.core.common.sub.contacts.sub.contact.root import Contact -from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository -from jupiter.core.common.sub.notes.root import Note -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.core.smart_lists.sub.item.root import SmartListItem +from jupiter.core.smart_lists.sub.item.service.load import ( + SmartListItemLoadResult, + SmartListItemLoadService, +) from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.storage.repository import DomainUnitOfWork -from jupiter.framework.use_case import ( - readonly_use_case, -) -from jupiter.framework.use_case_io import ( - UseCaseArgsBase, - UseCaseResultBase, - use_case_args, - use_case_result, -) -from jupiter.framework.utils.generic_loader import generic_loader +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + +__all__ = [ + "SmartListItemLoadArgs", + "SmartListItemLoadResult", + "SmartListItemLoadUseCase", +] @use_case_args @@ -36,16 +30,6 @@ class SmartListItemLoadArgs(UseCaseArgsBase): allow_archived: bool | None -@use_case_result -class SmartListItemLoadResult(UseCaseResultBase): - """SmartListItemLoadResult.""" - - item: SmartListItem - generic_tags: list[Tag] - contacts: list[Contact] - note: Note | None - - @readonly_use_case(WorkspaceFeature.SMART_LISTS) class SmartListItemLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[ @@ -62,43 +46,14 @@ async def _perform_transactional_read( ) -> SmartListItemLoadResult: """Execute the command's action.""" allow_archived = args.allow_archived or False - item, note = await generic_loader( - uow, - SmartListItem, - args.ref_id, - SmartListItem.note, - allow_archived=allow_archived, - allow_subentity_archived=allow_archived, - ) - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.SMART_LIST_ITEM.value, item.ref_id), - ) - if tag_link is not None: - generic_tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - generic_tags = [] - contact_domain = await uow.get_for(ContactDomain).load_by_parent( - context.workspace.ref_id, - ) - contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.SMART_LIST_ITEM.value, item.ref_id), + + item = await uow.get_for(SmartListItem).load_by_id( + args.ref_id, allow_archived=allow_archived ) - if contact_link is not None: - contacts = await uow.get_for(Contact).find_all_generic( - parent_ref_id=contact_domain.ref_id, - allow_archived=False, - ref_id=contact_link.contacts_ref_ids, - ) - else: - contacts = [] - return SmartListItemLoadResult( - item=item, - generic_tags=generic_tags, - contacts=contacts, - note=note, + return await SmartListItemLoadService().do_it( + uow, + context.workspace.ref_id, + item, + allow_archived=allow_archived, ) diff --git a/src/core/jupiter/core/smart_lists/sub/item/use_case/load_public.py b/src/core/jupiter/core/smart_lists/sub/item/use_case/load_public.py new file mode 100644 index 000000000..aeebe6aee --- /dev/null +++ b/src/core/jupiter/core/smart_lists/sub/item/use_case/load_public.py @@ -0,0 +1,85 @@ +"""Guest readonly use case for loading a published smart list item.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.smart_lists.collection import SmartListCollection +from jupiter.core.smart_lists.root import SmartList +from jupiter.core.smart_lists.sub.item.root import SmartListItem +from jupiter.core.smart_lists.sub.item.service.load import ( + SmartListItemLoadResult, + SmartListItemLoadService, +) +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class SmartListItemLoadPublicArgs(UseCaseArgsBase): + """SmartListItemLoadPublic args.""" + + external_id: PublishExternalId + + +class SmartListItemLoadPublicUseCase( + JupiterGuestReadonlyUseCase[SmartListItemLoadPublicArgs, SmartListItemLoadResult] +): + """Load a published smart list item by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: SmartListItemLoadPublicArgs, + ) -> SmartListItemLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.SMART_LIST_ITEM.value: + raise InputValidationError( + "The publish entity does not refer to a smart list item." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + smart_list_collection = await uow.get_for( + SmartListCollection + ).load_by_parent(publish_domain.workspace.ref_id) + item = await uow.get_for(SmartListItem).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + smart_list = await uow.get_for(SmartList).load_by_id( + item.smart_list.ref_id, + allow_archived=False, + ) + if smart_list.parent_ref_id != smart_list_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace smart list item." + ) + + return await SmartListItemLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + item, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx index b9fc4747b..5287e39d0 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -25,6 +25,14 @@ function publishedEntityLocation(externalId: string, owner: string): string { return `/app/public/published/journal/${externalId}`; case NamedEntityTag.TIME_PLAN: return `/app/public/published/time-plan/${externalId}`; + case NamedEntityTag.SCHEDULE_EVENT_IN_DAY: + return `/app/public/published/schedule-event-in-day/${externalId}`; + case NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS: + return `/app/public/published/schedule-event-full-days/${externalId}`; + case NamedEntityTag.SMART_LIST_ITEM: + return `/app/public/published/smart-list-item/${externalId}`; + case NamedEntityTag.METRIC_ENTRY: + return `/app/public/published/metric-entry/${externalId}`; default: throw new Response(ReasonPhrases.NOT_FOUND, { status: StatusCodes.NOT_FOUND, diff --git a/src/webui/app/routes/app/public/published/metric-entry/$externalId.tsx b/src/webui/app/routes/app/public/published/metric-entry/$externalId.tsx new file mode 100644 index 000000000..9c35c2e3c --- /dev/null +++ b/src/webui/app/routes/app/public/published/metric-entry/$externalId.tsx @@ -0,0 +1,100 @@ +import { ApiError } from "@jupiter/webapi-client"; +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { MetricEntryEditor } from "@jupiter/core/metrics/sub/entry/component/editor"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.metrics.metricEntryLoadPublic({ + external_id: externalId, + }); + + return json({ + metricEntry: result.metric_entry, + tags: result.tags ?? [], + contacts: result.contacts ?? [], + note: result.note ?? null, + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedMetricEntry() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const { metricEntry, tags, contacts, note } = loaderData; + + return ( + <LeafPanel + key={`published-metric-entry-${metricEntry.ref_id}`} + fakeKey={`published-metric-entry-${metricEntry.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <MetricEntryEditor + metricEntry={metricEntry} + tags={tags} + contacts={contacts} + allTags={tags} + allContacts={contacts} + inputsEnabled={false} + topLevelInfo={topLevelInfo} + /> + + <SectionCard title="Note"> + {note ? ( + <EntityNoteEditor initialNote={note} inputsEnabled={false} /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => + `Could not find published metric entry ${params.externalId}!`, + error: (params) => + `There was an error loading published metric entry ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx b/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx new file mode 100644 index 000000000..9a1534c15 --- /dev/null +++ b/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx @@ -0,0 +1,113 @@ +import { ApiError } from "@jupiter/webapi-client"; +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { ScheduleEventFullDaysEditor } from "@jupiter/core/schedule/sub/event_full_days/component/editor"; +import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_full_days/root"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.schedule.scheduleEventFullDaysLoadPublic({ + external_id: externalId, + }); + + return json({ + scheduleEventFullDays: result.schedule_event_full_days, + timeEventFullDaysBlock: result.time_event_full_days_block, + scheduleStream: result.schedule_stream, + note: result.note ?? null, + tags: result.tags ?? [], + contacts: result.contacts ?? [], + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedScheduleEventFullDays() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const { + scheduleEventFullDays, + timeEventFullDaysBlock, + scheduleStream, + note, + tags, + contacts, + } = loaderData; + + return ( + <LeafPanel + key={`published-schedule-event-full-days-${scheduleEventFullDays.ref_id}`} + fakeKey={`published-schedule-event-full-days-${scheduleEventFullDays.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <ScheduleEventFullDaysEditor + scheduleEventFullDays={scheduleEventFullDays} + timeEventFullDaysBlock={timeEventFullDaysBlock} + allScheduleStreams={[scheduleStream]} + tags={tags} + contacts={contacts} + allTags={tags} + allContacts={contacts} + inputsEnabled={false} + corePropertyEditable={isCorePropertyEditable(scheduleEventFullDays)} + topLevelInfo={topLevelInfo} + /> + + <SectionCard title="Note"> + {note ? ( + <EntityNoteEditor initialNote={note} inputsEnabled={false} /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => + `Could not find published schedule event ${params.externalId}!`, + error: (params) => + `There was an error loading published schedule event ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx b/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx new file mode 100644 index 000000000..39093ff39 --- /dev/null +++ b/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx @@ -0,0 +1,129 @@ +import { ApiError } from "@jupiter/webapi-client"; +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext, useMemo } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { timeEventInDayBlockParamsToTimezone } from "@jupiter/core/common/sub/time_events/time-event"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { ScheduleEventInDayEditor } from "@jupiter/core/schedule/sub/event_in_day/component/editor"; +import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_in_day/root"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.schedule.scheduleEventInDayLoadPublic({ + external_id: externalId, + }); + + return json({ + scheduleEventInDay: result.schedule_event_in_day, + timeEventInDayBlock: result.time_event_in_day_block, + scheduleStream: result.schedule_stream, + note: result.note ?? null, + tags: result.tags ?? [], + contacts: result.contacts ?? [], + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedScheduleEventInDay() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const { + scheduleEventInDay, + timeEventInDayBlock, + scheduleStream, + note, + tags, + contacts, + } = loaderData; + + const blockParamsInTz = useMemo( + () => + timeEventInDayBlockParamsToTimezone( + { + startDate: timeEventInDayBlock.start_date, + startTimeInDay: timeEventInDayBlock.start_time_in_day, + }, + topLevelInfo.user.timezone, + ), + [timeEventInDayBlock, topLevelInfo.user.timezone], + ); + + return ( + <LeafPanel + key={`published-schedule-event-in-day-${scheduleEventInDay.ref_id}`} + fakeKey={`published-schedule-event-in-day-${scheduleEventInDay.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <ScheduleEventInDayEditor + scheduleEventInDay={scheduleEventInDay} + timeEventInDayBlock={timeEventInDayBlock} + allScheduleStreams={[scheduleStream]} + tags={tags} + contacts={contacts} + allTags={tags} + allContacts={contacts} + inputsEnabled={false} + corePropertyEditable={isCorePropertyEditable(scheduleEventInDay)} + topLevelInfo={topLevelInfo} + startDate={blockParamsInTz.startDate} + startTimeInDay={blockParamsInTz.startTimeInDay!} + durationMins={timeEventInDayBlock.duration_mins} + /> + + <SectionCard title="Note"> + {note ? ( + <EntityNoteEditor initialNote={note} inputsEnabled={false} /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => + `Could not find published schedule event ${params.externalId}!`, + error: (params) => + `There was an error loading published schedule event ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/public/published/smart-list-item/$externalId.tsx b/src/webui/app/routes/app/public/published/smart-list-item/$externalId.tsx new file mode 100644 index 000000000..e3d62515d --- /dev/null +++ b/src/webui/app/routes/app/public/published/smart-list-item/$externalId.tsx @@ -0,0 +1,100 @@ +import { ApiError } from "@jupiter/webapi-client"; +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { SmartListItemEditor } from "@jupiter/core/smart_lists/sub/item/component/editor"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.smartLists.smartListItemLoadPublic({ + external_id: externalId, + }); + + return json({ + item: result.item, + genericTags: result.generic_tags ?? [], + contacts: result.contacts ?? [], + note: result.note ?? null, + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedSmartListItem() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const { item, genericTags, contacts, note } = loaderData; + + return ( + <LeafPanel + key={`published-smart-list-item-${item.ref_id}`} + fakeKey={`published-smart-list-item-${item.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <SmartListItemEditor + item={item} + genericTags={genericTags} + contacts={contacts} + allTags={genericTags} + allContacts={contacts} + inputsEnabled={false} + topLevelInfo={topLevelInfo} + /> + + <SectionCard title="Note"> + {note ? ( + <EntityNoteEditor initialNote={note} inputsEnabled={false} /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => + `Could not find published smart list item ${params.externalId}!`, + error: (params) => + `There was an error loading published smart list item ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/workspace/calendar/schedule/event-full-days/$id.tsx b/src/webui/app/routes/app/workspace/calendar/schedule/event-full-days/$id.tsx index acab8af2c..15722ab7a 100644 --- a/src/webui/app/routes/app/workspace/calendar/schedule/event-full-days/$id.tsx +++ b/src/webui/app/routes/app/workspace/calendar/schedule/event-full-days/$id.tsx @@ -1,13 +1,5 @@ import type { ScheduleStreamSummary } from "@jupiter/webapi-client"; import { NamedEntityTag, ApiError, Contact, Tag } from "@jupiter/webapi-client"; -import { - Button, - ButtonGroup, - FormControl, - InputLabel, - OutlinedInput, - Stack, -} from "@mui/material"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; @@ -23,22 +15,17 @@ import { parseForm, parseParams } from "zodix"; import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_full_days/root"; import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; -import { FieldError, GlobalError } from "@jupiter/core/infra/component/errors"; +import { GlobalError } from "@jupiter/core/infra/component/errors"; import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; import { - ActionMultipleSpread, ActionSingle, SectionActions, } from "@jupiter/core/infra/component/section-actions"; import { SectionCard } from "@jupiter/core/infra/component/section-card"; -import { ScheduleStreamSelect } from "@jupiter/core/schedule/component/select"; +import { ScheduleEventFullDaysEditor } from "@jupiter/core/schedule/sub/event_full_days/component/editor"; import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; -import { useBigScreen } from "@jupiter/core/infra/component/use-big-screen"; -import { TagsEditor } from "@jupiter/core/common/sub/tags/component/tags-editor"; -import { ContactsEditor } from "@jupiter/core/common/sub/contacts/component/contacts-editor"; -import { entityLinkStd } from "@jupiter/core/common/entity-link"; import { noteStdOwner } from "#/core/common/sub/notes/note-std-owner"; import { basicShouldRevalidate } from "~/rendering/standard-should-revalidate"; @@ -69,6 +56,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -111,6 +110,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ).contacts ?? [], allTags: allTags.tags as Array<Tag>, allContacts: allContacts.contacts as Array<Contact>, + publishEntity: response.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -185,6 +185,36 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/calendar?${url.searchParams}`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect( + `/app/workspace/calendar/schedule/event-full-days/${id}?${url.searchParams}`, + ); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect( + `/app/workspace/calendar/schedule/event-full-days/${id}?${url.searchParams}`, + ); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect( + `/app/workspace/calendar/schedule/event-full-days/${id}?${url.searchParams}`, + ); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -208,8 +238,6 @@ export default function ScheduleEventFullDaysViewOne() { const topLevelInfo = useContext(TopLevelInfoContext); const navigation = useNavigation(); const [query] = useSearchParams(); - const isBigScreen = useBigScreen(); - const inputsEnabled = navigation.state === "idle" && !loaderData.scheduleEventFullDays.archived; const corePropertyEditable = isCorePropertyEditable( @@ -223,10 +251,6 @@ export default function ScheduleEventFullDaysViewOne() { setDurationDays(loaderData.timeEventFullDaysBlock.duration_days); }, [loaderData.timeEventFullDaysBlock.duration_days]); - const allScheduleStreamsByRefId = new Map( - loaderData.allScheduleStreams.map((st) => [st.ref_id, st]), - ); - return ( <LeafPanel key={`schedule-event-full-days-${loaderData.scheduleEventFullDays.ref_id}`} @@ -238,166 +262,25 @@ export default function ScheduleEventFullDaysViewOne() { entityNotEditable={!corePropertyEditable} entityArchived={loaderData.scheduleEventFullDays.archived} returnLocation={`/app/workspace/calendar?${query}`} + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <GlobalError actionResult={actionData} /> - <SectionCard - id="schedule-event-full-days-properties" - title="Properties" - actions={ - <SectionActions - id="schedule-event-full-days-properties" - topLevelInfo={topLevelInfo} - inputsEnabled={inputsEnabled} - actions={[ - ActionMultipleSpread({ - actions: [ - ActionSingle({ - text: "Save", - value: "update", - highlight: true, - disabled: !corePropertyEditable, - }), - ActionSingle({ - text: "Change Stream", - value: "change-schedule-stream", - disabled: !corePropertyEditable, - }), - ], - }), - ]} - /> - } - > - <FormControl fullWidth> - <InputLabel id="scheduleStreamRefId">Schedule Stream</InputLabel> - <ScheduleStreamSelect - labelId="scheduleStreamRefId" - label="Schedule Stream" - name="scheduleStreamRefId" - readOnly={!inputsEnabled || !corePropertyEditable} - allScheduleStreams={loaderData.allScheduleStreams} - defaultValue={ - allScheduleStreamsByRefId.get( - loaderData.scheduleEventFullDays.schedule_stream_ref_id, - )! - } - /> - <FieldError - actionResult={actionData} - fieldName="/schedule_stream_ref_id" - /> - </FormControl> - <Stack - direction={isBigScreen ? "row" : "column"} - spacing={2} - useFlexGap - > - <FormControl fullWidth={!isBigScreen} sx={{ flexGrow: 1 }}> - <InputLabel id="name">Name</InputLabel> - <OutlinedInput - label="name" - name="name" - readOnly={!inputsEnabled || !corePropertyEditable} - defaultValue={loaderData.scheduleEventFullDays.name} - /> - <FieldError actionResult={actionData} fieldName="/name" /> - </FormControl> - </Stack> - - <Stack direction={isBigScreen ? "row" : "column"} useFlexGap gap={2}> - <FormControl fullWidth sx={{ flexGrow: 1 }}> - <TagsEditor - name="tags_names" - allTags={loaderData.allTags} - defaultValue={loaderData.tags.map((t) => t.ref_id)} - inputsEnabled={inputsEnabled} - owner={entityLinkStd( - NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS, - loaderData.scheduleEventFullDays.ref_id, - )} - aloneOnLine={!isBigScreen} - /> - </FormControl> - - <FormControl fullWidth sx={{ flexGrow: 1 }}> - <ContactsEditor - name="contacts_names" - allContacts={loaderData.allContacts} - defaultValue={loaderData.contacts.map( - (contact) => contact.ref_id, - )} - inputsEnabled={inputsEnabled} - owner={entityLinkStd( - NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS, - loaderData.scheduleEventFullDays.ref_id, - )} - aloneOnLine={!isBigScreen} - /> - </FormControl> - </Stack> - - <FormControl fullWidth> - <InputLabel id="startDate" shrink margin="dense"> - Start Date - </InputLabel> - <OutlinedInput - type="date" - notched - label="startDate" - name="startDate" - readOnly={!inputsEnabled || !corePropertyEditable} - disabled={!inputsEnabled || !corePropertyEditable} - defaultValue={loaderData.timeEventFullDaysBlock.start_date} - /> - - <FieldError actionResult={actionData} fieldName="/start_date" /> - </FormControl> - - <Stack spacing={2} direction="row"> - <ButtonGroup - variant="outlined" - disabled={!inputsEnabled || !corePropertyEditable} - > - <Button - disabled={!inputsEnabled || !corePropertyEditable} - variant={durationDays === 1 ? "contained" : "outlined"} - onClick={() => setDurationDays(1)} - > - 1D - </Button> - <Button - disabled={!inputsEnabled || !corePropertyEditable} - variant={durationDays === 3 ? "contained" : "outlined"} - onClick={() => setDurationDays(3)} - > - 3d - </Button> - <Button - disabled={!inputsEnabled || !corePropertyEditable} - variant={durationDays === 7 ? "contained" : "outlined"} - onClick={() => setDurationDays(7)} - > - 7d - </Button> - </ButtonGroup> - - <FormControl fullWidth> - <InputLabel id="durationDays" shrink margin="dense"> - Duration (Days) - </InputLabel> - <OutlinedInput - type="number" - label="Duration (Days)" - name="durationDays" - readOnly={!inputsEnabled || !corePropertyEditable} - value={durationDays} - onChange={(e) => setDurationDays(parseInt(e.target.value, 10))} - /> - - <FieldError actionResult={actionData} fieldName="/duration_days" /> - </FormControl> - </Stack> - </SectionCard> + <ScheduleEventFullDaysEditor + scheduleEventFullDays={loaderData.scheduleEventFullDays} + timeEventFullDaysBlock={loaderData.timeEventFullDaysBlock} + allScheduleStreams={loaderData.allScheduleStreams} + tags={loaderData.tags} + contacts={loaderData.contacts} + allTags={loaderData.allTags} + allContacts={loaderData.allContacts} + inputsEnabled={inputsEnabled} + corePropertyEditable={corePropertyEditable} + topLevelInfo={topLevelInfo} + actionResult={actionData} + durationDays={durationDays} + onDurationDaysChange={setDurationDays} + /> <SectionCard title="Note" diff --git a/src/webui/app/routes/app/workspace/calendar/schedule/event-in-day/$id.tsx b/src/webui/app/routes/app/workspace/calendar/schedule/event-in-day/$id.tsx index 80997eb82..853339cf9 100644 --- a/src/webui/app/routes/app/workspace/calendar/schedule/event-in-day/$id.tsx +++ b/src/webui/app/routes/app/workspace/calendar/schedule/event-in-day/$id.tsx @@ -1,13 +1,5 @@ import type { ScheduleStreamSummary } from "@jupiter/webapi-client"; import { NamedEntityTag, ApiError, Contact, Tag } from "@jupiter/webapi-client"; -import { - Button, - ButtonGroup, - FormControl, - InputLabel, - OutlinedInput, - Stack, -} from "@mui/material"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; @@ -27,23 +19,18 @@ import { import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_in_day/root"; import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; -import { FieldError, GlobalError } from "@jupiter/core/infra/component/errors"; +import { GlobalError } from "@jupiter/core/infra/component/errors"; import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; import { - ActionMultipleSpread, ActionSingle, SectionActions, } from "@jupiter/core/infra/component/section-actions"; import { SectionCard } from "@jupiter/core/infra/component/section-card"; -import { ScheduleStreamSelect } from "@jupiter/core/schedule/component/select"; +import { ScheduleEventInDayEditor } from "@jupiter/core/schedule/sub/event_in_day/component/editor"; import { TimeEventParamsSource } from "@jupiter/core/common/sub/time_events/component/params-source"; import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; -import { useBigScreen } from "@jupiter/core/infra/component/use-big-screen"; -import { TagsEditor } from "@jupiter/core/common/sub/tags/component/tags-editor"; -import { ContactsEditor } from "@jupiter/core/common/sub/contacts/component/contacts-editor"; -import { entityLinkStd } from "@jupiter/core/common/entity-link"; import { noteStdOwner } from "#/core/common/sub/notes/note-std-owner"; import { basicShouldRevalidate } from "~/rendering/standard-should-revalidate"; @@ -76,6 +63,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -118,6 +117,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ).contacts ?? [], allTags: allTags.tags as Array<Tag>, allContacts: allContacts.contacts as Array<Contact>, + publishEntity: response.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -200,6 +200,36 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/calendar?${url.searchParams}`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect( + `/app/workspace/calendar/schedule/event-in-day/${id}?${url.searchParams}`, + ); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect( + `/app/workspace/calendar/schedule/event-in-day/${id}?${url.searchParams}`, + ); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect( + `/app/workspace/calendar/schedule/event-in-day/${id}?${url.searchParams}`, + ); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -223,8 +253,6 @@ export default function ScheduleEventInDayViewOne() { const topLevelInfo = useContext(TopLevelInfoContext); const navigation = useNavigation(); const [query] = useSearchParams(); - const isBigScreen = useBigScreen(); - const inputsEnabled = navigation.state === "idle" && !loaderData.scheduleEventInDay.archived; const corePropertyEditable = isCorePropertyEditable( @@ -274,10 +302,6 @@ export default function ScheduleEventInDayViewOne() { } }, [query, debounceForeign]); - const allScheduleStreamsByRefId = new Map( - loaderData.allScheduleStreams.map((st) => [st.ref_id, st]), - ); - return ( <LeafPanel key={`schedule-event-in-day-${loaderData.scheduleEventInDay.ref_id}`} @@ -289,6 +313,8 @@ export default function ScheduleEventInDayViewOne() { entityNotEditable={!corePropertyEditable} entityArchived={loaderData.scheduleEventInDay.archived} returnLocation={`/app/workspace/calendar?${query}`} + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <TimeEventParamsSource startDate={startDate} @@ -296,194 +322,25 @@ export default function ScheduleEventInDayViewOne() { durationMins={durationMins} /> <GlobalError actionResult={actionData} /> - <SectionCard - id="schedule-event-in-day-properties" - title="Properties" - actions={ - <SectionActions - id="schedule-event-in-day-properties" - topLevelInfo={topLevelInfo} - inputsEnabled={inputsEnabled} - actions={[ - ActionMultipleSpread({ - actions: [ - ActionSingle({ - text: "Save", - value: "update", - highlight: true, - disabled: !corePropertyEditable, - }), - ActionSingle({ - text: "Change Stream", - value: "change-schedule-stream", - disabled: !corePropertyEditable, - }), - ], - }), - ]} - /> - } - > - <input - type="hidden" - name="userTimezone" - value={topLevelInfo.user.timezone} - /> - <FormControl fullWidth> - <InputLabel id="scheduleStreamRefId">Schedule Stream</InputLabel> - <ScheduleStreamSelect - labelId="scheduleStreamRefId" - label="Schedule Stream" - name="scheduleStreamRefId" - readOnly={!inputsEnabled || !corePropertyEditable} - allScheduleStreams={loaderData.allScheduleStreams} - defaultValue={ - allScheduleStreamsByRefId.get( - loaderData.scheduleEventInDay.schedule_stream_ref_id, - )! - } - /> - <FieldError - actionResult={actionData} - fieldName="/schedule_stream_ref_id" - /> - </FormControl> - - <FormControl fullWidth={!isBigScreen} sx={{ flexGrow: 1 }}> - <InputLabel id="name">Name</InputLabel> - <OutlinedInput - label="name" - name="name" - readOnly={!inputsEnabled || !corePropertyEditable} - defaultValue={loaderData.scheduleEventInDay.name} - /> - <FieldError actionResult={actionData} fieldName="/name" /> - </FormControl> - - <Stack direction={isBigScreen ? "row" : "column"} useFlexGap gap={2}> - <FormControl fullWidth sx={{ flexGrow: 1 }}> - <TagsEditor - name="tags_names" - allTags={loaderData.allTags} - defaultValue={loaderData.tags.map((t) => t.ref_id)} - inputsEnabled={inputsEnabled} - owner={entityLinkStd( - NamedEntityTag.SCHEDULE_EVENT_IN_DAY, - loaderData.scheduleEventInDay.ref_id, - )} - aloneOnLine={!isBigScreen} - /> - </FormControl> - - <FormControl fullWidth sx={{ flexGrow: 1 }}> - <ContactsEditor - name="contacts_names" - allContacts={loaderData.allContacts} - defaultValue={loaderData.contacts.map( - (contact) => contact.ref_id, - )} - inputsEnabled={inputsEnabled} - owner={entityLinkStd( - NamedEntityTag.SCHEDULE_EVENT_IN_DAY, - loaderData.scheduleEventInDay.ref_id, - )} - aloneOnLine={!isBigScreen} - /> - </FormControl> - </Stack> - - <Stack direction="row" useFlexGap gap={2}> - <FormControl fullWidth> - <InputLabel id="startDate" shrink margin="dense"> - Start Date - </InputLabel> - <OutlinedInput - type="date" - notched - label="startDate" - name="startDate" - readOnly={!inputsEnabled || !corePropertyEditable} - disabled={!inputsEnabled || !corePropertyEditable} - value={startDate} - onChange={(e) => setStartDate(e.target.value)} - /> - - <FieldError actionResult={actionData} fieldName="/start_date" /> - </FormControl> - - <FormControl fullWidth> - <InputLabel id="startTimeInDay" shrink margin="dense"> - Start Time - </InputLabel> - <OutlinedInput - type="time" - label="startTimeInDay" - name="startTimeInDay" - readOnly={!inputsEnabled || !corePropertyEditable} - value={startTimeInDay} - onChange={(e) => setStartTimeInDay(e.target.value)} - /> - - <FieldError - actionResult={actionData} - fieldName="/start_time_in_day" - /> - </FormControl> - </Stack> - - <Stack spacing={2} direction="row"> - <ButtonGroup - variant="outlined" - disabled={!inputsEnabled || !corePropertyEditable} - > - <Button - disabled={!inputsEnabled || !corePropertyEditable} - variant={durationMins === 15 ? "contained" : "outlined"} - onClick={() => setDurationMins(15)} - > - 15m - </Button> - <Button - disabled={!inputsEnabled || !corePropertyEditable} - variant={durationMins === 30 ? "contained" : "outlined"} - onClick={() => setDurationMins(30)} - > - 30m - </Button> - <Button - disabled={!inputsEnabled || !corePropertyEditable} - variant={durationMins === 60 ? "contained" : "outlined"} - onClick={() => setDurationMins(60)} - > - 60m - </Button> - </ButtonGroup> - - <FormControl fullWidth> - <InputLabel id="durationMins" shrink margin="dense"> - Duration (Mins) - </InputLabel> - <OutlinedInput - type="number" - label="Duration (Mins)" - name="durationMins" - readOnly={!inputsEnabled || !corePropertyEditable} - value={durationMins} - onChange={(e) => { - if (Number.isNaN(parseInt(e.target.value, 10))) { - setDurationMins(0); - e.preventDefault(); - return; - } - - return setDurationMins(parseInt(e.target.value, 10)); - }} - /> - - <FieldError actionResult={actionData} fieldName="/duration_mins" /> - </FormControl> - </Stack> - </SectionCard> + <ScheduleEventInDayEditor + scheduleEventInDay={loaderData.scheduleEventInDay} + timeEventInDayBlock={loaderData.timeEventInDayBlock} + allScheduleStreams={loaderData.allScheduleStreams} + tags={loaderData.tags} + contacts={loaderData.contacts} + allTags={loaderData.allTags} + allContacts={loaderData.allContacts} + inputsEnabled={inputsEnabled} + corePropertyEditable={corePropertyEditable} + topLevelInfo={topLevelInfo} + actionResult={actionData} + startDate={startDate} + startTimeInDay={startTimeInDay} + durationMins={durationMins} + onStartDateChange={setStartDate} + onStartTimeInDayChange={setStartTimeInDay} + onDurationMinsChange={setDurationMins} + /> <SectionCard title="Note" diff --git a/src/webui/app/routes/app/workspace/metrics/$id/entries/$entryId.tsx b/src/webui/app/routes/app/workspace/metrics/$id/entries/$entryId.tsx index 62ff67762..358b2123d 100644 --- a/src/webui/app/routes/app/workspace/metrics/$id/entries/$entryId.tsx +++ b/src/webui/app/routes/app/workspace/metrics/$id/entries/$entryId.tsx @@ -1,5 +1,4 @@ import { ApiError, Contact, NamedEntityTag, Tag } from "@jupiter/webapi-client"; -import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; @@ -8,12 +7,10 @@ import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseForm, parseParams } from "zodix"; -import { aDateToDate } from "@jupiter/core/common/adate"; import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; -import { FieldError, GlobalError } from "@jupiter/core/infra/component/errors"; +import { GlobalError } from "@jupiter/core/infra/component/errors"; import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; -import { TimeDiffTag } from "@jupiter/core/common/component/time-diff-tag"; import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; @@ -22,10 +19,7 @@ import { SectionActions, } from "@jupiter/core/infra/component/section-actions"; import { SectionCard } from "@jupiter/core/infra/component/section-card"; -import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; -import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; -import { entityLinkStd } from "@jupiter/core/common/entity-link"; -import { useBigScreen } from "@jupiter/core/infra/component/use-big-screen"; +import { MetricEntryEditor } from "@jupiter/core/metrics/sub/entry/component/editor"; import { noteStdOwner } from "#/core/common/sub/notes/note-std-owner"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -52,6 +46,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -87,6 +93,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ).contacts ?? [], allTags: allTags.tags as Array<Tag>, allContacts: allContacts.contacts as Array<Contact>, + publishEntity: result.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -148,6 +155,30 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/metrics/${id}`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/metrics/${id}/entries/${entryId}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/metrics/${id}/entries/${entryId}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/metrics/${id}/entries/${entryId}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -172,7 +203,6 @@ export default function MetricEntry() { const actionData = useActionData<typeof action>(); const navigation = useNavigation(); const topLevelInfo = useContext(TopLevelInfoContext); - const isBigScreen = useBigScreen(); const inputsEnabled = navigation.state === "idle" && !loaderData.metricEntry.archived; @@ -187,100 +217,20 @@ export default function MetricEntry() { inputsEnabled={inputsEnabled} entityArchived={loaderData.metricEntry.archived} returnLocation={`/app/workspace/metrics/${id}`} + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <GlobalError actionResult={actionData} /> - <SectionCard - title="Properties" - actions={ - <SectionActions - id="metric-entry-properties" - topLevelInfo={topLevelInfo} - inputsEnabled={inputsEnabled} - actions={[ - ActionSingle({ - text: "Save", - value: "update", - highlight: true, - }), - ]} - /> - } - > - <Stack direction={isBigScreen ? "row" : "column"} spacing={2}> - <TimeDiffTag - today={topLevelInfo.today} - labelPrefix="Collected" - collectionTime={loaderData.metricEntry.collection_time} - /> - - <FormControl fullWidth sx={{ flexGrow: 1 }}> - <TagsEditor - name="tags" - label={null} - aloneOnLine - allTags={loaderData.allTags} - defaultValue={loaderData.tags.map((tag) => tag.ref_id)} - inputsEnabled={inputsEnabled} - owner={entityLinkStd( - NamedEntityTag.METRIC_ENTRY, - loaderData.metricEntry.ref_id, - )} - /> - </FormControl> - - <FormControl fullWidth sx={{ flexGrow: 1 }}> - <ContactsEditor - name="contacts_names" - label={null} - aloneOnLine - allContacts={loaderData.allContacts} - defaultValue={loaderData.contacts.map( - (contact) => contact.ref_id, - )} - inputsEnabled={inputsEnabled} - owner={entityLinkStd( - NamedEntityTag.METRIC_ENTRY, - loaderData.metricEntry.ref_id, - )} - /> - </FormControl> - </Stack> - <FormControl fullWidth> - <InputLabel id="collectionTime" shrink> - Collection Time - </InputLabel> - <OutlinedInput - type="date" - notched - label="collectionTime" - defaultValue={ - loaderData.metricEntry.collection_time - ? aDateToDate(loaderData.metricEntry.collection_time).toFormat( - "yyyy-MM-dd", - ) - : undefined - } - name="collectionTime" - readOnly={!inputsEnabled} - disabled={!inputsEnabled} - /> - - <FieldError actionResult={actionData} fieldName="/collection_time" /> - </FormControl> - - <FormControl fullWidth> - <InputLabel id="value">Value</InputLabel> - <OutlinedInput - type="number" - inputProps={{ step: "any" }} - label="Value" - name="value" - readOnly={!inputsEnabled} - defaultValue={loaderData.metricEntry.value} - /> - <FieldError actionResult={actionData} fieldName="/value" /> - </FormControl> - </SectionCard> + <MetricEntryEditor + metricEntry={loaderData.metricEntry} + tags={loaderData.tags} + contacts={loaderData.contacts} + allTags={loaderData.allTags} + allContacts={loaderData.allContacts} + inputsEnabled={inputsEnabled} + topLevelInfo={topLevelInfo} + actionResult={actionData} + /> <SectionCard title="Note" diff --git a/src/webui/app/routes/app/workspace/smart-lists/$id/$itemId.tsx b/src/webui/app/routes/app/workspace/smart-lists/$id/$itemId.tsx index b1baa2f80..f903d16a7 100644 --- a/src/webui/app/routes/app/workspace/smart-lists/$id/$itemId.tsx +++ b/src/webui/app/routes/app/workspace/smart-lists/$id/$itemId.tsx @@ -1,13 +1,5 @@ import type { Contact, Tag } from "@jupiter/webapi-client"; import { NamedEntityTag, ApiError } from "@jupiter/webapi-client"; -import { - FormControl, - FormControlLabel, - InputLabel, - OutlinedInput, - Switch, - Stack, -} from "@mui/material"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; @@ -18,12 +10,8 @@ import { CheckboxAsString, parseForm, parseParams } from "zodix"; import { useContext } from "react"; import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; -import { FieldError, GlobalError } from "@jupiter/core/infra/component/errors"; +import { GlobalError } from "@jupiter/core/infra/component/errors"; import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; -import { TagsEditor } from "@jupiter/core/common/sub/tags/component/tags-editor"; -import { ContactsEditor } from "@jupiter/core/common/sub/contacts/component/contacts-editor"; -import { entityLinkStd } from "@jupiter/core/common/entity-link"; -import { useBigScreen } from "@jupiter/core/infra/component/use-big-screen"; import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; @@ -32,6 +20,7 @@ import { ActionSingle, SectionActions, } from "@jupiter/core/infra/component/section-actions"; +import { SmartListItemEditor } from "@jupiter/core/smart_lists/sub/item/component/editor"; import { noteStdOwner } from "#/core/common/sub/notes/note-std-owner"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -62,6 +51,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -97,6 +98,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { allTags: allTags.tags as Array<Tag>, allContacts: allContacts.contacts as Array<Contact>, note: result.note, + publishEntity: result.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -165,6 +167,30 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/smart-lists/${id}`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/smart-lists/${id}/${itemId}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/smart-lists/${id}/${itemId}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/smart-lists/${id}/${itemId}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -186,7 +212,6 @@ export default function SmartListItem() { const actionData = useActionData<typeof action>(); const navigation = useNavigation(); const topLevelInfo = useContext(TopLevelInfoContext); - const isBigScreen = useBigScreen(); const inputsEnabled = navigation.state === "idle" && !loaderData.item.archived; @@ -201,97 +226,20 @@ export default function SmartListItem() { inputsEnabled={inputsEnabled} entityArchived={loaderData.item.archived} returnLocation={`/app/workspace/smart-lists/${id}`} + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <GlobalError actionResult={actionData} /> - <SectionCard - title="Properties" - actions={ - <SectionActions - id="email-task-actions" - topLevelInfo={topLevelInfo} - inputsEnabled={inputsEnabled} - actions={[ - ActionSingle({ - text: "Save", - value: "update", - highlight: true, - }), - ]} - /> - } - > - <Stack direction="row" useFlexGap spacing={1}> - <FormControl fullWidth sx={{ flexGrow: 1 }}> - <InputLabel id="name">Name</InputLabel> - <OutlinedInput - label="Name" - defaultValue={loaderData.item.name} - name="name" - readOnly={!inputsEnabled} - /> - - <FieldError actionResult={actionData} fieldName="/name" /> - </FormControl> - </Stack> - - <Stack direction={isBigScreen ? "row" : "column"} spacing={2}> - <FormControl sx={{ flexGrow: 1, minWidth: "25%" }}> - <TagsEditor - name="generic_tags_names" - aloneOnLine - allTags={loaderData.allTags} - defaultValue={loaderData.genericTags.map((t) => t.ref_id)} - inputsEnabled={inputsEnabled} - owner={entityLinkStd( - NamedEntityTag.SMART_LIST_ITEM, - loaderData.item.ref_id, - )} - label="Tags" - /> - </FormControl> - - <FormControl sx={{ flexGrow: 1, minWidth: "25%" }}> - <ContactsEditor - name="contacts_names" - aloneOnLine - allContacts={loaderData.allContacts} - defaultValue={loaderData.contacts.map((c) => c.ref_id)} - inputsEnabled={inputsEnabled} - owner={entityLinkStd( - NamedEntityTag.SMART_LIST_ITEM, - loaderData.item.ref_id, - )} - label="Contacts" - /> - </FormControl> - </Stack> - - <FormControl fullWidth> - <FormControlLabel - control={ - <Switch - name="isDone" - readOnly={!inputsEnabled} - disabled={!inputsEnabled} - defaultChecked={loaderData.item.is_done} - /> - } - label="Is Done" - /> - <FieldError actionResult={actionData} fieldName="/is_done" /> - </FormControl> - - <FormControl fullWidth> - <InputLabel id="url">Url [Optional]</InputLabel> - <OutlinedInput - label="Url" - name="url" - readOnly={!inputsEnabled} - defaultValue={loaderData.item.url} - /> - <FieldError actionResult={actionData} fieldName="/url" /> - </FormControl> - </SectionCard> + <SmartListItemEditor + item={loaderData.item} + genericTags={loaderData.genericTags} + contacts={loaderData.contacts} + allTags={loaderData.allTags} + allContacts={loaderData.allContacts} + inputsEnabled={inputsEnabled} + topLevelInfo={topLevelInfo} + actionResult={actionData} + /> <SectionCard title="Note" From 0b9debe4974b84a25cbb30fcc52cc1190a3d85fb Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Mon, 8 Jun 2026 09:55:51 +0300 Subject: [PATCH 07/21] Added support for all --- .../api/docs/doc_load_public.py | 212 +++++++++++++++ .../api/prm/person_load_public.py | 212 +++++++++++++++ .../api/publish/publish_entity_find.py | 212 +++++++++++++++ .../api/publish/publish_entity_load.py | 212 +++++++++++++++ .../publish/publish_entity_load_settings.py | 212 +++++++++++++++ .../jupiter_webapi_client/models/__init__.py | 16 ++ .../models/doc_load_public_args.py | 62 +++++ .../models/doc_load_result.py | 54 ++-- .../models/person_load_public_args.py | 62 +++++ .../models/person_load_result.py | 51 ++++ .../models/publish_entity_find_args.py | 105 ++++++++ .../models/publish_entity_find_result.py | 76 ++++++ .../models/publish_entity_load_args.py | 84 ++++++ .../models/publish_entity_load_result.py | 68 +++++ .../publish_entity_load_settings_args.py | 47 ++++ .../publish_entity_load_settings_result.py | 62 +++++ gen/ts/webapi-client/gen/index.ts | 8 + .../gen/models/DocLoadPublicArgs.ts | 12 + .../webapi-client/gen/models/DocLoadResult.ts | 3 +- .../gen/models/PersonLoadPublicArgs.ts | 12 + .../gen/models/PersonLoadResult.ts | 4 + .../gen/models/PublishEntityFindArgs.ts | 13 + .../gen/models/PublishEntityFindResult.ts | 12 + .../gen/models/PublishEntityLoadArgs.ts | 13 + .../gen/models/PublishEntityLoadResult.ts | 12 + .../models/PublishEntityLoadSettingsArgs.ts | 10 + .../models/PublishEntityLoadSettingsResult.ts | 11 + .../webapi-client/gen/services/DocsService.ts | 29 ++ .../webapi-client/gen/services/PrmService.ts | 29 ++ .../gen/services/PublishService.ts | 90 +++++++ itests/webui/entities/docs.test.py | 35 +++ itests/webui/entities/persons.test.py | 34 ++- itests/webui/entities/schedules.test.py | 4 +- itests/webui/entities/smart_list.test.py | 4 +- .../recurring-task-gen-params-block.tsx | 4 +- .../components/publish-owner-type-chip.tsx | 14 + .../sub/publish/publish-owner-type-name.ts | 17 ++ .../common/sub/publish/sub/entity/root.py | 4 +- .../sub/publish/sub/entity/use_case/find.py | 65 +++++ .../sub/publish/sub/entity/use_case/load.py | 67 +++++ .../sub/entity/use_case/load_settings.py | 48 ++++ .../jupiter/core/docs/sub/doc/service/load.py | 73 +++++ .../core/docs/sub/doc/use_case/load.py | 45 +--- .../core/docs/sub/doc/use_case/load_public.py | 76 ++++++ .../infra/component/layout/branch-panel.tsx | 4 +- .../infra/component/layout/leaf-panel.tsx | 12 +- .../jupiter/core/infra/component/sidebar.tsx | 11 + .../core/journals/component/editor.tsx | 10 +- .../metrics/sub/entry/component/editor.tsx | 16 +- .../core/prm/sub/person/component/editor.tsx | 91 +++++++ .../core/prm/sub/person/service/load.py | 252 ++++++++++++++++++ .../person/sub/occasion/components/stack.tsx | 36 ++- .../core/prm/sub/person/use_case/load.py | 206 +------------- .../prm/sub/person/use_case/load_public.py | 77 ++++++ .../sub/event_full_days/component/editor.tsx | 13 +- .../sub/event_full_days/service/load.py | 2 +- .../sub/event_in_day/component/editor.tsx | 18 +- .../schedule/sub/event_in_day/service/load.py | 2 +- .../sub/event_in_day/use_case/load_public.py | 5 +- .../smart_lists/sub/item/component/editor.tsx | 1 + .../core/time_plans/component/editor.tsx | 10 +- .../core/vacations/component/editor.tsx | 1 + .../app/public/published/$externalId.tsx | 4 + .../app/public/published/doc/$externalId.tsx | 93 +++++++ .../public/published/person/$externalId.tsx | 125 +++++++++ .../published/time-plan/$externalId.tsx | 8 +- .../public/published/vacation/$externalId.tsx | 11 +- .../app/routes/app/workspace/core/publish.tsx | 198 ++++++++++++++ .../core/publish/$publishEntityId.tsx | 156 +++++++++++ .../app/workspace/docs/$dirId/doc/$docId.tsx | 39 +++ .../routes/app/workspace/prm/persons/$id.tsx | 113 ++++---- .../routes/app/workspace/time-plans/$id.tsx | 9 +- .../app/routes/app/workspace/todos/$id.tsx | 2 +- 73 files changed, 3631 insertions(+), 389 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/docs/doc_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/prm/person_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_find.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load_settings.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/doc_load_public_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/person_load_public_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_find_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_find_result.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_result.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_settings_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_settings_result.py create mode 100644 gen/ts/webapi-client/gen/models/DocLoadPublicArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/PersonLoadPublicArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityFindArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityFindResult.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityLoadArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityLoadResult.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityLoadSettingsArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/PublishEntityLoadSettingsResult.ts create mode 100644 src/core/jupiter/core/common/sub/publish/components/publish-owner-type-chip.tsx create mode 100644 src/core/jupiter/core/common/sub/publish/publish-owner-type-name.ts create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/use_case/find.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load.py create mode 100644 src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load_settings.py create mode 100644 src/core/jupiter/core/docs/sub/doc/service/load.py create mode 100644 src/core/jupiter/core/docs/sub/doc/use_case/load_public.py create mode 100644 src/core/jupiter/core/prm/sub/person/component/editor.tsx create mode 100644 src/core/jupiter/core/prm/sub/person/service/load.py create mode 100644 src/core/jupiter/core/prm/sub/person/use_case/load_public.py create mode 100644 src/webui/app/routes/app/public/published/doc/$externalId.tsx create mode 100644 src/webui/app/routes/app/public/published/person/$externalId.tsx create mode 100644 src/webui/app/routes/app/workspace/core/publish.tsx create mode 100644 src/webui/app/routes/app/workspace/core/publish/$publishEntityId.tsx diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/docs/doc_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/docs/doc_load_public.py new file mode 100644 index 000000000..0b7574924 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/docs/doc_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.doc_load_public_args import DocLoadPublicArgs +from ...models.doc_load_result import DocLoadResult +from ...models.error_response import ErrorResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: DocLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/doc-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> DocLoadResult | ErrorResponse | None: + if response.status_code == 200: + response_200 = DocLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[DocLoadResult | ErrorResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: DocLoadPublicArgs | Unset = UNSET, +) -> Response[DocLoadResult | ErrorResponse]: + """Load a published doc by publish external id. + + Args: + body (DocLoadPublicArgs | Unset): DocLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DocLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: DocLoadPublicArgs | Unset = UNSET, +) -> DocLoadResult | ErrorResponse | None: + """Load a published doc by publish external id. + + Args: + body (DocLoadPublicArgs | Unset): DocLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DocLoadResult | ErrorResponse + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: DocLoadPublicArgs | Unset = UNSET, +) -> Response[DocLoadResult | ErrorResponse]: + """Load a published doc by publish external id. + + Args: + body (DocLoadPublicArgs | Unset): DocLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DocLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: DocLoadPublicArgs | Unset = UNSET, +) -> DocLoadResult | ErrorResponse | None: + """Load a published doc by publish external id. + + Args: + body (DocLoadPublicArgs | Unset): DocLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DocLoadResult | ErrorResponse + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/prm/person_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/prm/person_load_public.py new file mode 100644 index 000000000..45d7ecc49 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/prm/person_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.person_load_public_args import PersonLoadPublicArgs +from ...models.person_load_result import PersonLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: PersonLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/person-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | PersonLoadResult | None: + if response.status_code == 200: + response_200 = PersonLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | PersonLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: PersonLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | PersonLoadResult]: + """Load a published person by publish external id. + + Args: + body (PersonLoadPublicArgs | Unset): PersonLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PersonLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: PersonLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | PersonLoadResult | None: + """Load a published person by publish external id. + + Args: + body (PersonLoadPublicArgs | Unset): PersonLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PersonLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: PersonLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | PersonLoadResult]: + """Load a published person by publish external id. + + Args: + body (PersonLoadPublicArgs | Unset): PersonLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PersonLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: PersonLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | PersonLoadResult | None: + """Load a published person by publish external id. + + Args: + body (PersonLoadPublicArgs | Unset): PersonLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PersonLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_find.py b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_find.py new file mode 100644 index 000000000..96421fe0a --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_find.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.publish_entity_find_args import PublishEntityFindArgs +from ...models.publish_entity_find_result import PublishEntityFindResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: PublishEntityFindArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/publish-entity-find", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | PublishEntityFindResult | None: + if response.status_code == 200: + response_200 = PublishEntityFindResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | PublishEntityFindResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityFindArgs | Unset = UNSET, +) -> Response[ErrorResponse | PublishEntityFindResult]: + """Use case for finding publish entities. + + Args: + body (PublishEntityFindArgs | Unset): PublishEntityFind args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PublishEntityFindResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: PublishEntityFindArgs | Unset = UNSET, +) -> ErrorResponse | PublishEntityFindResult | None: + """Use case for finding publish entities. + + Args: + body (PublishEntityFindArgs | Unset): PublishEntityFind args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PublishEntityFindResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityFindArgs | Unset = UNSET, +) -> Response[ErrorResponse | PublishEntityFindResult]: + """Use case for finding publish entities. + + Args: + body (PublishEntityFindArgs | Unset): PublishEntityFind args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PublishEntityFindResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: PublishEntityFindArgs | Unset = UNSET, +) -> ErrorResponse | PublishEntityFindResult | None: + """Use case for finding publish entities. + + Args: + body (PublishEntityFindArgs | Unset): PublishEntityFind args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PublishEntityFindResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load.py b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load.py new file mode 100644 index 000000000..16f8394cc --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.publish_entity_load_args import PublishEntityLoadArgs +from ...models.publish_entity_load_result import PublishEntityLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: PublishEntityLoadArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/publish-entity-load", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | PublishEntityLoadResult | None: + if response.status_code == 200: + response_200 = PublishEntityLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | PublishEntityLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityLoadArgs | Unset = UNSET, +) -> Response[ErrorResponse | PublishEntityLoadResult]: + """Use case for loading a publish entity. + + Args: + body (PublishEntityLoadArgs | Unset): PublishEntityLoad args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PublishEntityLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: PublishEntityLoadArgs | Unset = UNSET, +) -> ErrorResponse | PublishEntityLoadResult | None: + """Use case for loading a publish entity. + + Args: + body (PublishEntityLoadArgs | Unset): PublishEntityLoad args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PublishEntityLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityLoadArgs | Unset = UNSET, +) -> Response[ErrorResponse | PublishEntityLoadResult]: + """Use case for loading a publish entity. + + Args: + body (PublishEntityLoadArgs | Unset): PublishEntityLoad args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PublishEntityLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: PublishEntityLoadArgs | Unset = UNSET, +) -> ErrorResponse | PublishEntityLoadResult | None: + """Use case for loading a publish entity. + + Args: + body (PublishEntityLoadArgs | Unset): PublishEntityLoad args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PublishEntityLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load_settings.py b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load_settings.py new file mode 100644 index 000000000..6f0d07355 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/publish/publish_entity_load_settings.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.publish_entity_load_settings_args import PublishEntityLoadSettingsArgs +from ...models.publish_entity_load_settings_result import PublishEntityLoadSettingsResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: PublishEntityLoadSettingsArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/publish-entity-load-settings", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | PublishEntityLoadSettingsResult | None: + if response.status_code == 200: + response_200 = PublishEntityLoadSettingsResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | PublishEntityLoadSettingsResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityLoadSettingsArgs | Unset = UNSET, +) -> Response[ErrorResponse | PublishEntityLoadSettingsResult]: + """Load workspace-scoped settings for the publish feature. + + Args: + body (PublishEntityLoadSettingsArgs | Unset): PublishEntityLoadSettings args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PublishEntityLoadSettingsResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: PublishEntityLoadSettingsArgs | Unset = UNSET, +) -> ErrorResponse | PublishEntityLoadSettingsResult | None: + """Load workspace-scoped settings for the publish feature. + + Args: + body (PublishEntityLoadSettingsArgs | Unset): PublishEntityLoadSettings args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PublishEntityLoadSettingsResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: PublishEntityLoadSettingsArgs | Unset = UNSET, +) -> Response[ErrorResponse | PublishEntityLoadSettingsResult]: + """Load workspace-scoped settings for the publish feature. + + Args: + body (PublishEntityLoadSettingsArgs | Unset): PublishEntityLoadSettings args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | PublishEntityLoadSettingsResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: PublishEntityLoadSettingsArgs | Unset = UNSET, +) -> ErrorResponse | PublishEntityLoadSettingsResult | None: + """Load workspace-scoped settings for the publish feature. + + Args: + body (PublishEntityLoadSettingsArgs | Unset): PublishEntityLoadSettings args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | PublishEntityLoadSettingsResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index 9939390ce..dc1060c9f 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -220,6 +220,7 @@ from .doc_find_result import DocFindResult from .doc_find_result_entry import DocFindResultEntry from .doc_load_args import DocLoadArgs +from .doc_load_public_args import DocLoadPublicArgs from .doc_load_result import DocLoadResult from .doc_remove_args import DocRemoveArgs from .doc_update_args import DocUpdateArgs @@ -595,6 +596,7 @@ from .person_find_result import PersonFindResult from .person_find_result_entry import PersonFindResultEntry from .person_load_args import PersonLoadArgs +from .person_load_public_args import PersonLoadPublicArgs from .person_load_result import PersonLoadResult from .person_load_result_occasion_tags_by_ref_id import PersonLoadResultOccasionTagsByRefId from .person_load_settings_args import PersonLoadSettingsArgs @@ -625,8 +627,14 @@ from .publish_entity_activate_args import PublishEntityActivateArgs from .publish_entity_create_args import PublishEntityCreateArgs from .publish_entity_create_result import PublishEntityCreateResult +from .publish_entity_find_args import PublishEntityFindArgs +from .publish_entity_find_result import PublishEntityFindResult +from .publish_entity_load_args import PublishEntityLoadArgs from .publish_entity_load_by_external_id_args import PublishEntityLoadByExternalIdArgs from .publish_entity_load_by_external_id_result import PublishEntityLoadByExternalIdResult +from .publish_entity_load_result import PublishEntityLoadResult +from .publish_entity_load_settings_args import PublishEntityLoadSettingsArgs +from .publish_entity_load_settings_result import PublishEntityLoadSettingsResult from .publish_entity_status import PublishEntityStatus from .publish_entity_to_draft_args import PublishEntityToDraftArgs from .push_generation_extra_info import PushGenerationExtraInfo @@ -1232,6 +1240,7 @@ "DocFindResult", "DocFindResultEntry", "DocLoadArgs", + "DocLoadPublicArgs", "DocLoadResult", "DocRemoveArgs", "DocsHelpSubject", @@ -1597,6 +1606,7 @@ "PersonFindResult", "PersonFindResultEntry", "PersonLoadArgs", + "PersonLoadPublicArgs", "PersonLoadResult", "PersonLoadResultOccasionTagsByRefId", "PersonLoadSettingsArgs", @@ -1625,8 +1635,14 @@ "PublishEntityActivateArgs", "PublishEntityCreateArgs", "PublishEntityCreateResult", + "PublishEntityFindArgs", + "PublishEntityFindResult", + "PublishEntityLoadArgs", "PublishEntityLoadByExternalIdArgs", "PublishEntityLoadByExternalIdResult", + "PublishEntityLoadResult", + "PublishEntityLoadSettingsArgs", + "PublishEntityLoadSettingsResult", "PublishEntityStatus", "PublishEntityToDraftArgs", "PushGenerationExtraInfo", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/doc_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/doc_load_public_args.py new file mode 100644 index 000000000..048304514 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/doc_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="DocLoadPublicArgs") + + +@_attrs_define +class DocLoadPublicArgs: + """DocLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + doc_load_public_args = cls( + external_id=external_id, + ) + + doc_load_public_args.additional_properties = d + return doc_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/doc_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/doc_load_result.py index b73305ccb..ec783efba 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/doc_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/doc_load_result.py @@ -1,14 +1,17 @@ from __future__ import annotations from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field +from ..types import UNSET, Unset + if TYPE_CHECKING: from ..models.doc import Doc from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag @@ -22,41 +25,47 @@ class DocLoadResult: Attributes: doc (Doc): A doc in the docbook. note (Note): A note in the notebook. - subdocs (list[Doc]): tags (list[Tag]): + publish_entity (None | PublishEntity | Unset): """ doc: Doc note: Note - subdocs: list[Doc] tags: list[Tag] + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.publish_entity import PublishEntity + doc = self.doc.to_dict() note = self.note.to_dict() - subdocs = [] - for subdocs_item_data in self.subdocs: - subdocs_item = subdocs_item_data.to_dict() - subdocs.append(subdocs_item) - tags = [] for tags_item_data in self.tags: tags_item = tags_item_data.to_dict() tags.append(tags_item) + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( { "doc": doc, "note": note, - "subdocs": subdocs, "tags": tags, } ) + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -64,6 +73,7 @@ def to_dict(self) -> dict[str, Any]: def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.doc import Doc from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag d = dict(src_dict) @@ -71,13 +81,6 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: note = Note.from_dict(d.pop("note")) - subdocs = [] - _subdocs = d.pop("subdocs") - for subdocs_item_data in _subdocs: - subdocs_item = Doc.from_dict(subdocs_item_data) - - subdocs.append(subdocs_item) - tags = [] _tags = d.pop("tags") for tags_item_data in _tags: @@ -85,11 +88,28 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: tags.append(tags_item) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + doc_load_result = cls( doc=doc, note=note, - subdocs=subdocs, tags=tags, + publish_entity=publish_entity, ) doc_load_result.additional_properties = d diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/person_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/person_load_public_args.py new file mode 100644 index 000000000..311e00061 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/person_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PersonLoadPublicArgs") + + +@_attrs_define +class PersonLoadPublicArgs: + """PersonLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + person_load_public_args = cls( + external_id=external_id, + ) + + person_load_public_args.additional_properties = d + return person_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/person_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/person_load_result.py index f8636d6d0..91b24a6e0 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/person_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/person_load_result.py @@ -9,12 +9,14 @@ from ..types import UNSET, Unset if TYPE_CHECKING: + from ..models.circle import Circle from ..models.contact import Contact from ..models.inbox_task import InboxTask from ..models.note import Note from ..models.occasion import Occasion from ..models.person import Person from ..models.person_load_result_occasion_tags_by_ref_id import PersonLoadResultOccasionTagsByRefId + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_full_days_block import TimeEventFullDaysBlock @@ -30,6 +32,7 @@ class PersonLoadResult: person (Person): A person. contact (Contact): A contact. circle_ref_ids (list[str]): + circles (list[Circle]): occasions (list[Occasion]): occasion_tags_by_ref_id (PersonLoadResultOccasionTagsByRefId): occasion_time_event_blocks (list[TimeEventFullDaysBlock]): @@ -41,11 +44,13 @@ class PersonLoadResult: occasion_tasks_page_size (int): tags (list[Tag]): note (None | Note | Unset): + publish_entity (None | PublishEntity | Unset): """ person: Person contact: Contact circle_ref_ids: list[str] + circles: list[Circle] occasions: list[Occasion] occasion_tags_by_ref_id: PersonLoadResultOccasionTagsByRefId occasion_time_event_blocks: list[TimeEventFullDaysBlock] @@ -57,10 +62,12 @@ class PersonLoadResult: occasion_tasks_page_size: int tags: list[Tag] note: None | Note | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.note import Note + from ..models.publish_entity import PublishEntity person = self.person.to_dict() @@ -68,6 +75,11 @@ def to_dict(self) -> dict[str, Any]: circle_ref_ids = self.circle_ref_ids + circles = [] + for circles_item_data in self.circles: + circles_item = circles_item_data.to_dict() + circles.append(circles_item) + occasions = [] for occasions_item_data in self.occasions: occasions_item = occasions_item_data.to_dict() @@ -111,6 +123,14 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -118,6 +138,7 @@ def to_dict(self) -> dict[str, Any]: "person": person, "contact": contact, "circle_ref_ids": circle_ref_ids, + "circles": circles, "occasions": occasions, "occasion_tags_by_ref_id": occasion_tags_by_ref_id, "occasion_time_event_blocks": occasion_time_event_blocks, @@ -132,17 +153,21 @@ def to_dict(self) -> dict[str, Any]: ) if note is not UNSET: field_dict["note"] = note + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.circle import Circle from ..models.contact import Contact from ..models.inbox_task import InboxTask from ..models.note import Note from ..models.occasion import Occasion from ..models.person import Person from ..models.person_load_result_occasion_tags_by_ref_id import PersonLoadResultOccasionTagsByRefId + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_full_days_block import TimeEventFullDaysBlock @@ -153,6 +178,13 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: circle_ref_ids = cast(list[str], d.pop("circle_ref_ids")) + circles = [] + _circles = d.pop("circles") + for circles_item_data in _circles: + circles_item = Circle.from_dict(circles_item_data) + + circles.append(circles_item) + occasions = [] _occasions = d.pop("occasions") for occasions_item_data in _occasions: @@ -215,10 +247,28 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + person_load_result = cls( person=person, contact=contact, circle_ref_ids=circle_ref_ids, + circles=circles, occasions=occasions, occasion_tags_by_ref_id=occasion_tags_by_ref_id, occasion_time_event_blocks=occasion_time_event_blocks, @@ -230,6 +280,7 @@ def _parse_note(data: object) -> None | Note | Unset: occasion_tasks_page_size=occasion_tasks_page_size, tags=tags, note=note, + publish_entity=publish_entity, ) person_load_result.additional_properties = d diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_find_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_find_args.py new file mode 100644 index 000000000..0fb439b8f --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_find_args.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="PublishEntityFindArgs") + + +@_attrs_define +class PublishEntityFindArgs: + """PublishEntityFind args. + + Attributes: + allow_archived (bool | None | Unset): + filter_ref_ids (list[str] | None | Unset): + """ + + allow_archived: bool | None | Unset = UNSET + filter_ref_ids: list[str] | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + allow_archived: bool | None | Unset + if isinstance(self.allow_archived, Unset): + allow_archived = UNSET + else: + allow_archived = self.allow_archived + + filter_ref_ids: list[str] | None | Unset + if isinstance(self.filter_ref_ids, Unset): + filter_ref_ids = UNSET + elif isinstance(self.filter_ref_ids, list): + filter_ref_ids = self.filter_ref_ids + + else: + filter_ref_ids = self.filter_ref_ids + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if allow_archived is not UNSET: + field_dict["allow_archived"] = allow_archived + if filter_ref_ids is not UNSET: + field_dict["filter_ref_ids"] = filter_ref_ids + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + + def _parse_allow_archived(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + allow_archived = _parse_allow_archived(d.pop("allow_archived", UNSET)) + + def _parse_filter_ref_ids(data: object) -> list[str] | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, list): + raise TypeError() + filter_ref_ids_type_0 = cast(list[str], data) + + return filter_ref_ids_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(list[str] | None | Unset, data) + + filter_ref_ids = _parse_filter_ref_ids(d.pop("filter_ref_ids", UNSET)) + + publish_entity_find_args = cls( + allow_archived=allow_archived, + filter_ref_ids=filter_ref_ids, + ) + + publish_entity_find_args.additional_properties = d + return publish_entity_find_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_find_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_find_result.py new file mode 100644 index 000000000..6c52010e8 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_find_result.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.publish_entity import PublishEntity + + +T = TypeVar("T", bound="PublishEntityFindResult") + + +@_attrs_define +class PublishEntityFindResult: + """PublishEntityFind result. + + Attributes: + publish_entities (list[PublishEntity]): + """ + + publish_entities: list[PublishEntity] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + publish_entities = [] + for publish_entities_item_data in self.publish_entities: + publish_entities_item = publish_entities_item_data.to_dict() + publish_entities.append(publish_entities_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "publish_entities": publish_entities, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.publish_entity import PublishEntity + + d = dict(src_dict) + publish_entities = [] + _publish_entities = d.pop("publish_entities") + for publish_entities_item_data in _publish_entities: + publish_entities_item = PublishEntity.from_dict(publish_entities_item_data) + + publish_entities.append(publish_entities_item) + + publish_entity_find_result = cls( + publish_entities=publish_entities, + ) + + publish_entity_find_result.additional_properties = d + return publish_entity_find_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_args.py new file mode 100644 index 000000000..c0614e52c --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_args.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="PublishEntityLoadArgs") + + +@_attrs_define +class PublishEntityLoadArgs: + """PublishEntityLoad args. + + Attributes: + ref_id (str): A generic entity id. + allow_archived (bool | None | Unset): + """ + + ref_id: str + allow_archived: bool | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + ref_id = self.ref_id + + allow_archived: bool | None | Unset + if isinstance(self.allow_archived, Unset): + allow_archived = UNSET + else: + allow_archived = self.allow_archived + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "ref_id": ref_id, + } + ) + if allow_archived is not UNSET: + field_dict["allow_archived"] = allow_archived + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + ref_id = d.pop("ref_id") + + def _parse_allow_archived(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + allow_archived = _parse_allow_archived(d.pop("allow_archived", UNSET)) + + publish_entity_load_args = cls( + ref_id=ref_id, + allow_archived=allow_archived, + ) + + publish_entity_load_args.additional_properties = d + return publish_entity_load_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_result.py new file mode 100644 index 000000000..f47e9a418 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_result.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.publish_entity import PublishEntity + + +T = TypeVar("T", bound="PublishEntityLoadResult") + + +@_attrs_define +class PublishEntityLoadResult: + """PublishEntityLoad result. + + Attributes: + publish_entity (PublishEntity): A publish entity. + """ + + publish_entity: PublishEntity + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + publish_entity = self.publish_entity.to_dict() + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "publish_entity": publish_entity, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.publish_entity import PublishEntity + + d = dict(src_dict) + publish_entity = PublishEntity.from_dict(d.pop("publish_entity")) + + publish_entity_load_result = cls( + publish_entity=publish_entity, + ) + + publish_entity_load_result.additional_properties = d + return publish_entity_load_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_settings_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_settings_args.py new file mode 100644 index 000000000..a4d29f8ff --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_settings_args.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PublishEntityLoadSettingsArgs") + + +@_attrs_define +class PublishEntityLoadSettingsArgs: + """PublishEntityLoadSettings args.""" + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + publish_entity_load_settings_args = cls() + + publish_entity_load_settings_args.additional_properties = d + return publish_entity_load_settings_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_settings_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_settings_result.py new file mode 100644 index 000000000..604f86165 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/publish_entity_load_settings_result.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="PublishEntityLoadSettingsResult") + + +@_attrs_define +class PublishEntityLoadSettingsResult: + """PublishEntityLoadSettings results. + + Attributes: + allowed_publish_owner_entity_types (list[str]): + """ + + allowed_publish_owner_entity_types: list[str] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + allowed_publish_owner_entity_types = self.allowed_publish_owner_entity_types + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "allowed_publish_owner_entity_types": allowed_publish_owner_entity_types, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + allowed_publish_owner_entity_types = cast(list[str], d.pop("allowed_publish_owner_entity_types")) + + publish_entity_load_settings_result = cls( + allowed_publish_owner_entity_types=allowed_publish_owner_entity_types, + ) + + publish_entity_load_settings_result.additional_properties = d + return publish_entity_load_settings_result + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index e9040fc1a..f229a19c9 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -187,6 +187,7 @@ export type { DocFindResult } from './models/DocFindResult'; export type { DocFindResultEntry } from './models/DocFindResultEntry'; export type { DocIdempotencyKey } from './models/DocIdempotencyKey'; export type { DocLoadArgs } from './models/DocLoadArgs'; +export type { DocLoadPublicArgs } from './models/DocLoadPublicArgs'; export type { DocLoadResult } from './models/DocLoadResult'; export type { DocName } from './models/DocName'; export type { DocRemoveArgs } from './models/DocRemoveArgs'; @@ -485,6 +486,7 @@ export type { PersonFindArgs } from './models/PersonFindArgs'; export type { PersonFindResult } from './models/PersonFindResult'; export type { PersonFindResultEntry } from './models/PersonFindResultEntry'; export type { PersonLoadArgs } from './models/PersonLoadArgs'; +export type { PersonLoadPublicArgs } from './models/PersonLoadPublicArgs'; export type { PersonLoadResult } from './models/PersonLoadResult'; export type { PersonLoadSettingsArgs } from './models/PersonLoadSettingsArgs'; export type { PersonLoadSettingsResult } from './models/PersonLoadSettingsResult'; @@ -501,8 +503,14 @@ export type { PublishEntity } from './models/PublishEntity'; export type { PublishEntityActivateArgs } from './models/PublishEntityActivateArgs'; export type { PublishEntityCreateArgs } from './models/PublishEntityCreateArgs'; export type { PublishEntityCreateResult } from './models/PublishEntityCreateResult'; +export type { PublishEntityFindArgs } from './models/PublishEntityFindArgs'; +export type { PublishEntityFindResult } from './models/PublishEntityFindResult'; +export type { PublishEntityLoadArgs } from './models/PublishEntityLoadArgs'; export type { PublishEntityLoadByExternalIdArgs } from './models/PublishEntityLoadByExternalIdArgs'; export type { PublishEntityLoadByExternalIdResult } from './models/PublishEntityLoadByExternalIdResult'; +export type { PublishEntityLoadResult } from './models/PublishEntityLoadResult'; +export type { PublishEntityLoadSettingsArgs } from './models/PublishEntityLoadSettingsArgs'; +export type { PublishEntityLoadSettingsResult } from './models/PublishEntityLoadSettingsResult'; export type { PublishEntityName } from './models/PublishEntityName'; export { PublishEntityStatus } from './models/PublishEntityStatus'; export type { PublishEntityToDraftArgs } from './models/PublishEntityToDraftArgs'; diff --git a/gen/ts/webapi-client/gen/models/DocLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/DocLoadPublicArgs.ts new file mode 100644 index 000000000..e9c5578c3 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/DocLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * DocLoadPublic args. + */ +export type DocLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/DocLoadResult.ts b/gen/ts/webapi-client/gen/models/DocLoadResult.ts index c19fc9bbf..d6156a728 100644 --- a/gen/ts/webapi-client/gen/models/DocLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/DocLoadResult.ts @@ -4,6 +4,7 @@ /* eslint-disable */ import type { Doc } from './Doc'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { Tag } from './Tag'; /** * DocLoad result. @@ -11,7 +12,7 @@ import type { Tag } from './Tag'; export type DocLoadResult = { doc: Doc; note: Note; - subdocs: Array<Doc>; tags: Array<Tag>; + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/models/PersonLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/PersonLoadPublicArgs.ts new file mode 100644 index 000000000..124e748f3 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PersonLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * PersonLoadPublic args. + */ +export type PersonLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/PersonLoadResult.ts b/gen/ts/webapi-client/gen/models/PersonLoadResult.ts index 5da51e44c..33cec48f4 100644 --- a/gen/ts/webapi-client/gen/models/PersonLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/PersonLoadResult.ts @@ -2,12 +2,14 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { Circle } from './Circle'; import type { Contact } from './Contact'; import type { EntityId } from './EntityId'; import type { InboxTask } from './InboxTask'; import type { Note } from './Note'; import type { Occasion } from './Occasion'; import type { Person } from './Person'; +import type { PublishEntity } from './PublishEntity'; import type { Tag } from './Tag'; import type { TimeEventFullDaysBlock } from './TimeEventFullDaysBlock'; /** @@ -17,6 +19,7 @@ export type PersonLoadResult = { person: Person; contact: Contact; circle_ref_ids: Array<EntityId>; + circles: Array<Circle>; occasions: Array<Occasion>; occasion_tags_by_ref_id: Record<string, Array<Tag>>; occasion_time_event_blocks: Array<TimeEventFullDaysBlock>; @@ -28,5 +31,6 @@ export type PersonLoadResult = { occasion_tasks_page_size: number; tags: Array<Tag>; note?: (Note | null); + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/models/PublishEntityFindArgs.ts b/gen/ts/webapi-client/gen/models/PublishEntityFindArgs.ts new file mode 100644 index 000000000..e30f8597e --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityFindArgs.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +/** + * PublishEntityFind args. + */ +export type PublishEntityFindArgs = { + allow_archived?: (boolean | null); + filter_ref_ids?: (Array<EntityId> | null); +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntityFindResult.ts b/gen/ts/webapi-client/gen/models/PublishEntityFindResult.ts new file mode 100644 index 000000000..bc8f627ec --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityFindResult.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishEntity } from './PublishEntity'; +/** + * PublishEntityFind result. + */ +export type PublishEntityFindResult = { + publish_entities: Array<PublishEntity>; +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntityLoadArgs.ts b/gen/ts/webapi-client/gen/models/PublishEntityLoadArgs.ts new file mode 100644 index 000000000..1a02c3ae5 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityLoadArgs.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +/** + * PublishEntityLoad args. + */ +export type PublishEntityLoadArgs = { + ref_id: EntityId; + allow_archived?: (boolean | null); +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntityLoadResult.ts b/gen/ts/webapi-client/gen/models/PublishEntityLoadResult.ts new file mode 100644 index 000000000..6689f3409 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityLoadResult.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishEntity } from './PublishEntity'; +/** + * PublishEntityLoad result. + */ +export type PublishEntityLoadResult = { + publish_entity: PublishEntity; +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntityLoadSettingsArgs.ts b/gen/ts/webapi-client/gen/models/PublishEntityLoadSettingsArgs.ts new file mode 100644 index 000000000..e2cbc193e --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityLoadSettingsArgs.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * PublishEntityLoadSettings args. + */ +export type PublishEntityLoadSettingsArgs = { +}; + diff --git a/gen/ts/webapi-client/gen/models/PublishEntityLoadSettingsResult.ts b/gen/ts/webapi-client/gen/models/PublishEntityLoadSettingsResult.ts new file mode 100644 index 000000000..ef1479b01 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/PublishEntityLoadSettingsResult.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * PublishEntityLoadSettings results. + */ +export type PublishEntityLoadSettingsResult = { + allowed_publish_owner_entity_types: Array<string>; +}; + diff --git a/gen/ts/webapi-client/gen/services/DocsService.ts b/gen/ts/webapi-client/gen/services/DocsService.ts index 8487462c6..69c0add9d 100644 --- a/gen/ts/webapi-client/gen/services/DocsService.ts +++ b/gen/ts/webapi-client/gen/services/DocsService.ts @@ -17,6 +17,7 @@ import type { DocCreateResult } from '../models/DocCreateResult'; import type { DocFindArgs } from '../models/DocFindArgs'; import type { DocFindResult } from '../models/DocFindResult'; import type { DocLoadArgs } from '../models/DocLoadArgs'; +import type { DocLoadPublicArgs } from '../models/DocLoadPublicArgs'; import type { DocLoadResult } from '../models/DocLoadResult'; import type { DocRemoveArgs } from '../models/DocRemoveArgs'; import type { DocUpdateArgs } from '../models/DocUpdateArgs'; @@ -304,6 +305,34 @@ export class DocsService { }, }); } + /** + * Load a published doc by publish external id. + * @param requestBody The input data + * @returns DocLoadResult Successful response + * @throws ApiError + */ + public docLoadPublic( + requestBody?: DocLoadPublicArgs, + ): CancelablePromise<DocLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/doc-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * The command for removing a doc. * @param requestBody The input data diff --git a/gen/ts/webapi-client/gen/services/PrmService.ts b/gen/ts/webapi-client/gen/services/PrmService.ts index 72261cc39..b5091455a 100644 --- a/gen/ts/webapi-client/gen/services/PrmService.ts +++ b/gen/ts/webapi-client/gen/services/PrmService.ts @@ -24,6 +24,7 @@ import type { PersonCreateResult } from '../models/PersonCreateResult'; import type { PersonFindArgs } from '../models/PersonFindArgs'; import type { PersonFindResult } from '../models/PersonFindResult'; import type { PersonLoadArgs } from '../models/PersonLoadArgs'; +import type { PersonLoadPublicArgs } from '../models/PersonLoadPublicArgs'; import type { PersonLoadResult } from '../models/PersonLoadResult'; import type { PersonLoadSettingsArgs } from '../models/PersonLoadSettingsArgs'; import type { PersonLoadSettingsResult } from '../models/PersonLoadSettingsResult'; @@ -454,6 +455,34 @@ export class PrmService { }, }); } + /** + * Load a published person by publish external id. + * @param requestBody The input data + * @returns PersonLoadResult Successful response + * @throws ApiError + */ + public personLoadPublic( + requestBody?: PersonLoadPublicArgs, + ): CancelablePromise<PersonLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/person-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * A use case for regenerating tasks associated with persons. * @param requestBody The input data diff --git a/gen/ts/webapi-client/gen/services/PublishService.ts b/gen/ts/webapi-client/gen/services/PublishService.ts index 685e2e39d..5a494ede1 100644 --- a/gen/ts/webapi-client/gen/services/PublishService.ts +++ b/gen/ts/webapi-client/gen/services/PublishService.ts @@ -5,8 +5,14 @@ import type { PublishEntityActivateArgs } from '../models/PublishEntityActivateArgs'; import type { PublishEntityCreateArgs } from '../models/PublishEntityCreateArgs'; import type { PublishEntityCreateResult } from '../models/PublishEntityCreateResult'; +import type { PublishEntityFindArgs } from '../models/PublishEntityFindArgs'; +import type { PublishEntityFindResult } from '../models/PublishEntityFindResult'; +import type { PublishEntityLoadArgs } from '../models/PublishEntityLoadArgs'; import type { PublishEntityLoadByExternalIdArgs } from '../models/PublishEntityLoadByExternalIdArgs'; import type { PublishEntityLoadByExternalIdResult } from '../models/PublishEntityLoadByExternalIdResult'; +import type { PublishEntityLoadResult } from '../models/PublishEntityLoadResult'; +import type { PublishEntityLoadSettingsArgs } from '../models/PublishEntityLoadSettingsArgs'; +import type { PublishEntityLoadSettingsResult } from '../models/PublishEntityLoadSettingsResult'; import type { PublishEntityToDraftArgs } from '../models/PublishEntityToDraftArgs'; import type { CancelablePromise } from '../core/CancelablePromise'; import type { BaseHttpRequest } from '../core/BaseHttpRequest'; @@ -68,6 +74,62 @@ export class PublishService { }, }); } + /** + * Use case for finding publish entities. + * @param requestBody The input data + * @returns PublishEntityFindResult Successful response + * @throws ApiError + */ + public publishEntityFind( + requestBody?: PublishEntityFindArgs, + ): CancelablePromise<PublishEntityFindResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/publish-entity-find', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } + /** + * Use case for loading a publish entity. + * @param requestBody The input data + * @returns PublishEntityLoadResult Successful response + * @throws ApiError + */ + public publishEntityLoad( + requestBody?: PublishEntityLoadArgs, + ): CancelablePromise<PublishEntityLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/publish-entity-load', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * Load a publish entity by its external id. * @param requestBody The input data @@ -96,6 +158,34 @@ export class PublishService { }, }); } + /** + * Load workspace-scoped settings for the publish feature. + * @param requestBody The input data + * @returns PublishEntityLoadSettingsResult Successful response + * @throws ApiError + */ + public publishEntityLoadSettings( + requestBody?: PublishEntityLoadSettingsArgs, + ): CancelablePromise<PublishEntityLoadSettingsResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/publish-entity-load-settings', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * Use case for moving a publish entity back to draft. * @param requestBody The input data diff --git a/itests/webui/entities/docs.test.py b/itests/webui/entities/docs.test.py index d35d2d8f6..a0c1c6250 100644 --- a/itests/webui/entities/docs.test.py +++ b/itests/webui/entities/docs.test.py @@ -45,6 +45,7 @@ from itests.helpers import ( get_parsed_from_response, + open_leaf_publish_panel, type_editorjs_content_and_wait_for_save, ) @@ -338,3 +339,37 @@ def test_webui_docs_dir_remove_nested_reflects_in_browser( page.goto(f"/app/workspace/docs/{root_dir_ref_id}") expect(page.locator(f"#dir-{parent.ref_id}")).to_have_count(0) expect(page.locator(f"#doc-{doc.ref_id}")).to_have_count(0) + + +def test_webui_doc_publish_and_view_public(page: Page, create_doc) -> None: + doc = create_doc("Published Doc", "Published doc body") + page.goto(f"/app/workspace/docs/{doc.parent_dir_ref_id}/doc/{doc.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Doc-publish") + page.locator("button[id='Doc-publish-create']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/docs/{doc.parent_dir_ref_id}/doc/{doc.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Doc-publish") + expect(page.locator("#Doc-publish")).to_contain_text("draft") + + page.locator("button[id='Doc-publish-toggle-status']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/docs/{doc.parent_dir_ref_id}/doc/{doc.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Doc-publish") + expect(page.locator("#Doc-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/doc/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="name"]')).to_have_value("Published Doc") diff --git a/itests/webui/entities/persons.test.py b/itests/webui/entities/persons.test.py index 95c444821..b84b49366 100644 --- a/itests/webui/entities/persons.test.py +++ b/itests/webui/entities/persons.test.py @@ -1,5 +1,7 @@ """Tests about persons.""" +import re + import pytest from jupiter_webapi_client.api.prm.person_create import ( sync_detailed as person_create_sync, @@ -17,7 +19,7 @@ ) from playwright.sync_api import Page, expect -from itests.helpers import get_parsed_from_response +from itests.helpers import get_parsed_from_response, open_leaf_publish_panel @pytest.fixture(autouse=True, scope="module") @@ -63,3 +65,33 @@ def test_webui_person_view_all(page: Page, create_person) -> None: expect(page.locator(f"#person-{person1.ref_id}")).to_contain_text("Person 1") expect(page.locator(f"#person-{person2.ref_id}")).to_contain_text("Person 2") expect(page.locator(f"#person-{person3.ref_id}")).to_contain_text("Person 3") + + +def test_webui_person_publish_and_view_public(page: Page, create_person) -> None: + person = create_person("Published Person") + page.goto(f"/app/workspace/prm/persons/{person.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Person-publish") + page.locator("button[id='Person-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/prm/persons/{person.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Person-publish") + expect(page.locator("#Person-publish")).to_contain_text("draft") + + page.locator("button[id='Person-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/prm/persons/{person.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Person-publish") + expect(page.locator("#Person-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/person/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="name"]')).to_have_value("Published Person") diff --git a/itests/webui/entities/schedules.test.py b/itests/webui/entities/schedules.test.py index 0c6a46c85..01934afc6 100644 --- a/itests/webui/entities/schedules.test.py +++ b/itests/webui/entities/schedules.test.py @@ -276,6 +276,8 @@ def test_webui_schedule_event_full_days_publish_and_view_public( page.wait_for_url(re.compile(r"/app/public/published/schedule-event-full-days/")) page.wait_for_selector("#leaf-panel") - expect(page.locator('input[name="name"]')).to_have_value("Published Full Days Event") + expect(page.locator('input[name="name"]')).to_have_value( + "Published Full Days Event" + ) expect(page.locator('input[name="startDate"]')).to_have_value("2024-07-01") expect(page.locator('input[name="durationDays"]')).to_have_value("3") diff --git a/itests/webui/entities/smart_list.test.py b/itests/webui/entities/smart_list.test.py index 6bec41fcf..c3020249e 100644 --- a/itests/webui/entities/smart_list.test.py +++ b/itests/webui/entities/smart_list.test.py @@ -169,4 +169,6 @@ def test_webui_smart_list_item_publish_and_view_public( page.wait_for_url(re.compile(r"/app/public/published/smart-list-item/")) page.wait_for_selector("#leaf-panel") - expect(page.locator('input[name="name"]')).to_have_value("Published Smart List Item") + expect(page.locator('input[name="name"]')).to_have_value( + "Published Smart List Item" + ) diff --git a/src/core/jupiter/core/common/component/recurring-task-gen-params-block.tsx b/src/core/jupiter/core/common/component/recurring-task-gen-params-block.tsx index 99e17d50d..ee70b08b0 100644 --- a/src/core/jupiter/core/common/component/recurring-task-gen-params-block.tsx +++ b/src/core/jupiter/core/common/component/recurring-task-gen-params-block.tsx @@ -16,7 +16,7 @@ import { import { useEffect, useState } from "react"; import { periodName } from "#/core/common/recurring-task-period"; -import type { SomeErrorNoData } from "#/core/infra/action-result"; +import type { ActionResult } from "#/core/infra/action-result"; import { useBigScreen } from "#/core/infra/component/use-big-screen"; import { DifficultySelect } from "#/core/common/component/difficulty-select"; import { EisenhowerSelect } from "#/core/common/component/eisenhower-select"; @@ -38,7 +38,7 @@ interface RecurringTaskGenParamsBlockProps { dueAtDay?: RecurringTaskDueAtDay | null; dueAtMonth?: RecurringTaskDueAtDay | null; skipRule?: RecurringTaskSkipRule | null; - actionData?: SomeErrorNoData; + actionData?: ActionResult<unknown>; } export function RecurringTaskGenParamsBlock( diff --git a/src/core/jupiter/core/common/sub/publish/components/publish-owner-type-chip.tsx b/src/core/jupiter/core/common/sub/publish/components/publish-owner-type-chip.tsx new file mode 100644 index 000000000..b6c52e97a --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/components/publish-owner-type-chip.tsx @@ -0,0 +1,14 @@ +import { parseEntityLinkStd } from "#/core/common/entity-link"; +import { publishOwnerEntityTagName } from "#/core/common/sub/publish/publish-owner-type-name"; +import { SlimChip } from "#/core/infra/component/chips"; + +interface PublishOwnerTypeChipProps { + owner: string; +} + +export function PublishOwnerTypeChip(props: PublishOwnerTypeChipProps) { + const { theType } = parseEntityLinkStd(props.owner); + return ( + <SlimChip label={publishOwnerEntityTagName(theType)} color="default" /> + ); +} diff --git a/src/core/jupiter/core/common/sub/publish/publish-owner-type-name.ts b/src/core/jupiter/core/common/sub/publish/publish-owner-type-name.ts new file mode 100644 index 000000000..5cd22fa68 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/publish-owner-type-name.ts @@ -0,0 +1,17 @@ +import { NamedEntityTag } from "@jupiter/webapi-client"; + +import { noteOwnerEntityTagName } from "#/core/common/sub/notes/note-owner-type-name"; + +/** Human-readable label for a publish entity owner tag. */ +export function publishOwnerEntityTagName(ownerType: string): string { + if (ownerType === "ScheduleEventFullDaysBlock") { + return "Schedule Event Full Days"; + } + + const tag = ownerType as NamedEntityTag; + if (Object.values(NamedEntityTag).includes(tag)) { + return noteOwnerEntityTagName(tag); + } + + return ownerType; +} diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index 1c0e24076..3fd91274e 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -34,7 +34,7 @@ NamedEntityTag.HABIT.value, NamedEntityTag.CHORE.value, NamedEntityTag.BIG_PLAN.value, - NamedEntityTag.DOC.value, + NamedEntityTag.DOC.value, # done NamedEntityTag.DIR.value, NamedEntityTag.JOURNAL.value, # done NamedEntityTag.VACATION.value, # done @@ -42,7 +42,7 @@ NamedEntityTag.SMART_LIST_ITEM.value, # done NamedEntityTag.METRIC.value, NamedEntityTag.METRIC_ENTRY.value, # done - NamedEntityTag.PERSON.value, + NamedEntityTag.PERSON.value, # done } ) diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/find.py b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/find.py new file mode 100644 index 000000000..82d7c4c57 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/find.py @@ -0,0 +1,65 @@ +"""Use case for finding publish entities.""" + +from jupiter.core.app import AppCore +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity +from jupiter.core.config import ( + JupiterLoggedInReadonlyContext, + JupiterTransactionalLoggedInReadOnlyUseCase, +) +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.entity import NoFilter +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import ( + UseCaseArgsBase, + UseCaseResultBase, + use_case_args, + use_case_result, +) + + +@use_case_args +class PublishEntityFindArgs(UseCaseArgsBase): + """PublishEntityFind args.""" + + allow_archived: bool | None + filter_ref_ids: list[EntityId] | None + + +@use_case_result +class PublishEntityFindResult(UseCaseResultBase): + """PublishEntityFind result.""" + + publish_entities: list[PublishEntity] + + +@readonly_use_case(exclude_component=[AppCore.CLI]) +class PublishEntityFindUseCase( + JupiterTransactionalLoggedInReadOnlyUseCase[ + PublishEntityFindArgs, PublishEntityFindResult + ] +): + """Use case for finding publish entities.""" + + async def _perform_transactional_read( + self, + uow: DomainUnitOfWork, + context: JupiterLoggedInReadonlyContext, + args: PublishEntityFindArgs, + ) -> PublishEntityFindResult: + """Execute the command's action.""" + allow_archived = args.allow_archived or False + + workspace = context.workspace + publish_domain = await uow.get_for(PublishDomain).load_by_parent( + workspace.ref_id + ) + + publish_entities = await uow.get_for(PublishEntity).find_all_generic( + parent_ref_id=publish_domain.ref_id, + allow_archived=allow_archived, + ref_id=args.filter_ref_ids or NoFilter(), + ) + + return PublishEntityFindResult(publish_entities=publish_entities) diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load.py b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load.py new file mode 100644 index 000000000..bf1be703a --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load.py @@ -0,0 +1,67 @@ +"""Use case for loading a publish entity.""" + +from jupiter.core.app import AppCore +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity +from jupiter.core.config import ( + JupiterLoggedInReadonlyContext, + JupiterTransactionalLoggedInReadOnlyUseCase, +) +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.errors import InputValidationError +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import ( + UseCaseArgsBase, + UseCaseResultBase, + use_case_args, + use_case_result, +) + + +@use_case_args +class PublishEntityLoadArgs(UseCaseArgsBase): + """PublishEntityLoad args.""" + + ref_id: EntityId + allow_archived: bool | None + + +@use_case_result +class PublishEntityLoadResult(UseCaseResultBase): + """PublishEntityLoad result.""" + + publish_entity: PublishEntity + + +@readonly_use_case(exclude_component=[AppCore.CLI]) +class PublishEntityLoadUseCase( + JupiterTransactionalLoggedInReadOnlyUseCase[ + PublishEntityLoadArgs, PublishEntityLoadResult + ] +): + """Use case for loading a publish entity.""" + + async def _perform_transactional_read( + self, + uow: DomainUnitOfWork, + context: JupiterLoggedInReadonlyContext, + args: PublishEntityLoadArgs, + ) -> PublishEntityLoadResult: + """Execute the command's action.""" + allow_archived = args.allow_archived or False + + publish_domain = await uow.get_for(PublishDomain).load_by_parent( + context.workspace.ref_id + ) + publish_entity = await uow.get_for(PublishEntity).load_by_id( + args.ref_id, + allow_archived=allow_archived, + ) + + if publish_entity.parent_ref_id != publish_domain.ref_id: + raise InputValidationError( + "The publish entity does not belong to this workspace." + ) + + return PublishEntityLoadResult(publish_entity=publish_entity) diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load_settings.py b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load_settings.py new file mode 100644 index 000000000..89101b34e --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/use_case/load_settings.py @@ -0,0 +1,48 @@ +"""Use case for loading publish-related settings.""" + +from jupiter.core.app import AppCore +from jupiter.core.common.sub.publish.sub.entity.root import ALLOWED_PUBLISH_OWNER_TYPES +from jupiter.core.config import ( + JupiterLoggedInReadonlyContext, + JupiterTransactionalLoggedInReadOnlyUseCase, +) +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import ( + UseCaseArgsBase, + UseCaseResultBase, + use_case_args, + use_case_result, +) + + +@use_case_args +class PublishEntityLoadSettingsArgs(UseCaseArgsBase): + """PublishEntityLoadSettings args.""" + + +@use_case_result +class PublishEntityLoadSettingsResult(UseCaseResultBase): + """PublishEntityLoadSettings results.""" + + allowed_publish_owner_entity_types: list[str] + + +@readonly_use_case(exclude_component=[AppCore.CLI]) +class PublishEntityLoadSettingsUseCase( + JupiterTransactionalLoggedInReadOnlyUseCase[ + PublishEntityLoadSettingsArgs, PublishEntityLoadSettingsResult + ] +): + """Load workspace-scoped settings for the publish feature.""" + + async def _perform_transactional_read( + self, + _uow: DomainUnitOfWork, + _context: JupiterLoggedInReadonlyContext, + _args: PublishEntityLoadSettingsArgs, + ) -> PublishEntityLoadSettingsResult: + """Execute the command's action.""" + return PublishEntityLoadSettingsResult( + allowed_publish_owner_entity_types=sorted(ALLOWED_PUBLISH_OWNER_TYPES), + ) diff --git a/src/core/jupiter/core/docs/sub/doc/service/load.py b/src/core/jupiter/core/docs/sub/doc/service/load.py new file mode 100644 index 000000000..79c529ce4 --- /dev/null +++ b/src/core/jupiter/core/docs/sub/doc/service/load.py @@ -0,0 +1,73 @@ +"""Shared service for loading a doc.""" + +from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.docs.sub.doc.root import Doc +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result + + +@use_case_result +class DocLoadResult(UseCaseResultBase): + """DocLoad result.""" + + doc: Doc + note: Note + tags: list[Tag] + publish_entity: PublishEntity | None + + +class DocLoadService: + """Shared service for loading a doc and its dependent entities.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + doc: Doc, + *, + allow_archived: bool = False, + include_publish_entity: bool = True, + ) -> DocLoadResult: + """Load a doc and its dependent entities.""" + doc = await uow.get_for(Doc).load_by_id( + doc.ref_id, allow_archived=allow_archived + ) + note = await uow.get(NoteRepository).load_for_owner( + EntityLink.std(NamedEntityTag.DOC.value, doc.ref_id), + allow_archived=allow_archived, + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std(NamedEntityTag.DOC.value, doc.ref_id), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std(NamedEntityTag.DOC.value, doc.ref_id), + allow_archived=allow_archived, + ) + + return DocLoadResult( + doc=doc, + note=note, + tags=tags, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/docs/sub/doc/use_case/load.py b/src/core/jupiter/core/docs/sub/doc/use_case/load.py index f1188559f..9e4ac5309 100644 --- a/src/core/jupiter/core/docs/sub/doc/use_case/load.py +++ b/src/core/jupiter/core/docs/sub/doc/use_case/load.py @@ -1,26 +1,19 @@ """Load a particular doc.""" from jupiter.core.app import AppCore -from jupiter.core.common.sub.notes.root import Note, NoteRepository -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.docs.sub.doc.root import Doc +from jupiter.core.docs.sub.doc.service.load import DocLoadResult, DocLoadService from jupiter.core.features import WorkspaceFeature -from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import readonly_use_case -from jupiter.framework.use_case_io import ( - UseCaseArgsBase, - UseCaseResultBase, - use_case_args, - use_case_result, -) +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + +__all__ = ["DocLoadArgs", "DocLoadResult", "DocLoadUseCase"] @use_case_args @@ -31,16 +24,6 @@ class DocLoadArgs(UseCaseArgsBase): allow_archived: bool | None -@use_case_result -class DocLoadResult(UseCaseResultBase): - """DocLoad result.""" - - doc: Doc - note: Note - subdocs: list[Doc] - tags: list[Tag] - - @readonly_use_case(WorkspaceFeature.DOCS, exclude_component=[AppCore.CLI]) class DocLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[DocLoadArgs, DocLoadResult] @@ -59,21 +42,9 @@ async def _perform_transactional_read( doc = await uow.get_for(Doc).load_by_id( args.ref_id, allow_archived=allow_archived ) - note = await uow.get(NoteRepository).load_for_owner( - EntityLink.std(NamedEntityTag.DOC.value, doc.ref_id), - allow_archived=allow_archived, - ) - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.DOC.value, doc.ref_id), + return await DocLoadService().do_it( + uow, + doc, + allow_archived=allow_archived, ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - - return DocLoadResult(doc, note, [], tags) diff --git a/src/core/jupiter/core/docs/sub/doc/use_case/load_public.py b/src/core/jupiter/core/docs/sub/doc/use_case/load_public.py new file mode 100644 index 000000000..0c5268c86 --- /dev/null +++ b/src/core/jupiter/core/docs/sub/doc/use_case/load_public.py @@ -0,0 +1,76 @@ +"""Guest readonly use case for loading a published doc.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.docs.root import DocCollection +from jupiter.core.docs.sub.doc.root import Doc +from jupiter.core.docs.sub.doc.service.load import DocLoadResult, DocLoadService +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class DocLoadPublicArgs(UseCaseArgsBase): + """DocLoadPublic args.""" + + external_id: PublishExternalId + + +class DocLoadPublicUseCase( + JupiterGuestReadonlyUseCase[DocLoadPublicArgs, DocLoadResult] +): + """Load a published doc by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: DocLoadPublicArgs, + ) -> DocLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.DOC.value: + raise InputValidationError( + "The publish entity does not refer to a doc." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + doc_collection = await uow.get_for(DocCollection).load_by_parent( + publish_domain.workspace.ref_id + ) + doc = await uow.get_for(Doc).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if doc.parent_ref_id != doc_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace doc." + ) + + return await DocLoadService().do_it( + uow, + doc, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/infra/component/layout/branch-panel.tsx b/src/core/jupiter/core/infra/component/layout/branch-panel.tsx index a7cf662d3..a7fb32d29 100644 --- a/src/core/jupiter/core/infra/component/layout/branch-panel.tsx +++ b/src/core/jupiter/core/infra/component/layout/branch-panel.tsx @@ -234,7 +234,9 @@ export function BranchPanel(props: PropsWithChildren<BranchPanelProps>) { setShowHistory((h) => !h); }} > - <HistoryIcon color={showHistory ? "primary" : undefined} /> + <HistoryIcon + color={showHistory ? "primary" : undefined} + /> </IconButton> )} </Box> diff --git a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx index eff61dcac..42564ca9a 100644 --- a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx +++ b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx @@ -26,9 +26,13 @@ import { Form, useNavigate } from "@remix-run/react"; import { motion, useIsPresent } from "framer-motion"; import type { PropsWithChildren } from "react"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; -import { PublishPanel } from "#/core/common/sub/publish/components/publish-panel"; -import { EntityId, NamedEntityTag, type PublishEntity } from "@jupiter/webapi-client"; +import { + EntityId, + NamedEntityTag, + type PublishEntity, +} from "@jupiter/webapi-client"; +import { PublishPanel } from "#/core/common/sub/publish/components/publish-panel"; import { LeafPanelExpansionState, LeafPanelExpansionStateContext, @@ -410,7 +414,9 @@ export function LeafPanel(props: PropsWithChildren<LeafPanelProps>) { <IconButton id="leaf-entity-archive" sx={ - !hasHistory && !hasPublish ? { marginLeft: "auto" } : undefined + !hasHistory && !hasPublish + ? { marginLeft: "auto" } + : undefined } disabled={ props.entityNotEditable || diff --git a/src/core/jupiter/core/infra/component/sidebar.tsx b/src/core/jupiter/core/infra/component/sidebar.tsx index 44cce2512..a6d9ae558 100644 --- a/src/core/jupiter/core/infra/component/sidebar.tsx +++ b/src/core/jupiter/core/infra/component/sidebar.tsx @@ -413,6 +413,17 @@ export default function Sidebar(props: SidebarProps) { </ListItemButton> </ListItem> + <ListItem disablePadding> + <ListItemButton + to="/app/workspace/core/publish" + component={Link} + onClick={onClickNavigation} + > + <ListItemIcon>🔗</ListItemIcon> + <ListItemText primary="Publish" /> + </ListItemButton> + </ListItem> + <ListItem disablePadding> <ListItemButton to="/app/workspace/mutation-history" diff --git a/src/core/jupiter/core/journals/component/editor.tsx b/src/core/jupiter/core/journals/component/editor.tsx index aaa212bce..e4d8960d3 100644 --- a/src/core/jupiter/core/journals/component/editor.tsx +++ b/src/core/jupiter/core/journals/component/editor.tsx @@ -1,6 +1,7 @@ import type { Journal, Tag } from "@jupiter/webapi-client"; import { NamedEntityTag } from "@jupiter/webapi-client"; import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; + import { entityLinkStd } from "#/core/common/entity-link"; import { PeriodSelect } from "#/core/common/component/period-select"; import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; @@ -27,8 +28,7 @@ interface JournalEditorProps { export function JournalEditor(props: JournalEditorProps) { const isBigScreen = useBigScreen(); const { journal, tags, allTags } = props; - const timeConfigEditable = - props.inputsEnabled && props.corePropertyEditable; + const timeConfigEditable = props.inputsEnabled && props.corePropertyEditable; return ( <SectionCard @@ -58,11 +58,7 @@ export function JournalEditor(props: JournalEditorProps) { ) : undefined } > - <Stack - direction={isBigScreen ? "row" : "column"} - spacing={2} - useFlexGap - > + <Stack direction={isBigScreen ? "row" : "column"} spacing={2} useFlexGap> <FormControl fullWidth> <InputLabel id="rightNow" shrink margin="dense"> The Date diff --git a/src/core/jupiter/core/metrics/sub/entry/component/editor.tsx b/src/core/jupiter/core/metrics/sub/entry/component/editor.tsx index 30d6095cd..8ab11eae5 100644 --- a/src/core/jupiter/core/metrics/sub/entry/component/editor.tsx +++ b/src/core/jupiter/core/metrics/sub/entry/component/editor.tsx @@ -1,6 +1,7 @@ import type { Contact, MetricEntry, Tag } from "@jupiter/webapi-client"; import { NamedEntityTag } from "@jupiter/webapi-client"; import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; + import { aDateToDate } from "#/core/common/adate"; import { entityLinkStd } from "#/core/common/entity-link"; import { TimeDiffTag } from "#/core/common/component/time-diff-tag"; @@ -67,7 +68,10 @@ export function MetricEntryEditor(props: MetricEntryEditorProps) { allTags={allTags} defaultValue={tags.map((tag) => tag.ref_id)} inputsEnabled={props.inputsEnabled} - owner={entityLinkStd(NamedEntityTag.METRIC_ENTRY, metricEntry.ref_id)} + owner={entityLinkStd( + NamedEntityTag.METRIC_ENTRY, + metricEntry.ref_id, + )} /> </FormControl> @@ -79,7 +83,10 @@ export function MetricEntryEditor(props: MetricEntryEditorProps) { allContacts={allContacts} defaultValue={contacts.map((contact) => contact.ref_id)} inputsEnabled={props.inputsEnabled} - owner={entityLinkStd(NamedEntityTag.METRIC_ENTRY, metricEntry.ref_id)} + owner={entityLinkStd( + NamedEntityTag.METRIC_ENTRY, + metricEntry.ref_id, + )} /> </FormControl> </Stack> @@ -102,7 +109,10 @@ export function MetricEntryEditor(props: MetricEntryEditorProps) { disabled={!props.inputsEnabled} /> - <FieldError actionResult={props.actionResult} fieldName="/collection_time" /> + <FieldError + actionResult={props.actionResult} + fieldName="/collection_time" + /> </FormControl> <FormControl fullWidth> diff --git a/src/core/jupiter/core/prm/sub/person/component/editor.tsx b/src/core/jupiter/core/prm/sub/person/component/editor.tsx new file mode 100644 index 000000000..92f5c6e57 --- /dev/null +++ b/src/core/jupiter/core/prm/sub/person/component/editor.tsx @@ -0,0 +1,91 @@ +import type { Circle, Contact, Person, Tag } from "@jupiter/webapi-client"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; + +import { entityLinkStd } from "#/core/common/entity-link"; +import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; +import type { ActionResult } from "#/core/infra/action-result"; +import { FieldError } from "#/core/infra/component/errors"; +import { RecurringTaskGenParamsBlock } from "#/core/common/component/recurring-task-gen-params-block"; +import { StandardDivider } from "#/core/infra/component/standard-divider"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import type { TopLevelInfo } from "#/core/infra/top-level-context"; +import { CircleMultiSelect } from "#/core/prm/sub/circle/components/multi-select"; + +interface PersonEditorProps { + person: Person; + contact: Contact; + tags: Array<Tag>; + allTags: Array<Tag>; + allCircles: Array<Circle>; + circleRefIds: Array<string>; + maxCirclesPerPerson: number; + inputsEnabled: boolean; + topLevelInfo: TopLevelInfo; + actionResult?: ActionResult<unknown>; +} + +export function PersonEditor(props: PersonEditorProps) { + const isBigScreen = useBigScreen(); + const { person, contact, tags, allTags, allCircles, circleRefIds } = props; + + return ( + <> + <Stack direction={isBigScreen ? "row" : "column"} spacing={1}> + <FormControl fullWidth sx={{ flexGrow: 3 }}> + <InputLabel id="name">Name</InputLabel> + <OutlinedInput + label="Name" + name="name" + readOnly={!props.inputsEnabled} + defaultValue={contact.name} + /> + <FieldError actionResult={props.actionResult} fieldName="/name" /> + </FormControl> + + <FormControl fullWidth sx={{ flexGrow: 2 }}> + <TagsEditor + name="tags" + label={null} + aloneOnLine={!isBigScreen} + allTags={allTags} + defaultValue={tags.map((tag) => tag.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.PERSON, person.ref_id)} + /> + </FormControl> + </Stack> + + <CircleMultiSelect + name="circleRefIds" + label="Circles" + inputsEnabled={props.inputsEnabled} + disabled={false} + allCircles={allCircles} + defaultValue={circleRefIds} + maxSelections={props.maxCirclesPerPerson} + /> + <FieldError + actionResult={props.actionResult} + fieldName="/circle_ref_ids" + /> + + <StandardDivider title="Catch Up" size="small" /> + + <RecurringTaskGenParamsBlock + namePrefix="catchUp" + fieldsPrefix="catch_up" + allowNonePeriod + period={person.catch_up_params?.period ?? "none"} + eisen={person.catch_up_params?.eisen} + difficulty={person.catch_up_params?.difficulty} + actionableFromDay={person.catch_up_params?.actionable_from_day} + actionableFromMonth={person.catch_up_params?.actionable_from_month} + dueAtDay={person.catch_up_params?.due_at_day} + dueAtMonth={person.catch_up_params?.due_at_month} + inputsEnabled={props.inputsEnabled} + actionData={props.actionResult} + /> + </> + ); +} diff --git a/src/core/jupiter/core/prm/sub/person/service/load.py b/src/core/jupiter/core/prm/sub/person/service/load.py new file mode 100644 index 000000000..5b2d32b0a --- /dev/null +++ b/src/core/jupiter/core/prm/sub/person/service/load.py @@ -0,0 +1,252 @@ +"""Shared service for loading a person.""" + +from typing import cast + +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.inbox_tasks.collection import InboxTaskCollection +from jupiter.core.common.sub.inbox_tasks.root import ( + InboxTask, + InboxTaskRepository, +) +from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.root import TagDomain +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( + TimeEventFullDaysBlock, + TimeEventFullDaysBlockRepository, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.prm.sub.circle.root import Circle +from jupiter.core.prm.sub.person.root import Person +from jupiter.core.prm.sub.person.sub.occasion.root import Occasion +from jupiter.core.prm.sub.person_circle_links.root import PersonCircleLink +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.entity import NoFilter +from jupiter.framework.errors import InputValidationError +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result + + +@use_case_result +class PersonLoadResult(UseCaseResultBase): + """PersonLoadResult.""" + + person: Person + contact: Contact + circle_ref_ids: list[EntityId] + circles: list[Circle] + occasions: list[Occasion] + occasion_tags_by_ref_id: dict[EntityId, list[Tag]] + occasion_time_event_blocks: list[TimeEventFullDaysBlock] + catch_up_tasks: list[InboxTask] + catch_up_tasks_total_cnt: int + catch_up_tasks_page_size: int + occasion_tasks: list[InboxTask] + occasion_tasks_total_cnt: int + occasion_tasks_page_size: int + tags: list[Tag] + note: Note | None + publish_entity: PublishEntity | None + + +class PersonLoadService: + """Shared service for loading a person and its dependent entities.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + person: Person, + *, + allow_archived: bool = False, + catch_up_task_retrieve_offset: int = 0, + occasion_task_retrieve_offset: int = 0, + include_inbox_tasks: bool = True, + include_occasion_time_events: bool = True, + include_publish_entity: bool = True, + ) -> PersonLoadResult: + """Load a person and its dependent entities.""" + person = await uow.get_for(Person).load_by_id( + person.ref_id, allow_archived=allow_archived + ) + contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.PERSON.value, person.ref_id), + ) + if contact_link is None or len(contact_link.contacts_ref_ids) == 0: + raise InputValidationError("Person does not have a linked contact") + contact = await uow.get_for(Contact).load_by_id( + contact_link.contacts_ref_ids[0] + ) + + occasions = await uow.get_for(Occasion).find_all_generic( + parent_ref_id=person.ref_id, + allow_archived=False, + ref_id=NoFilter(), + ) + + note = await uow.get(NoteRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.PERSON.value, person.ref_id), + allow_archived=allow_archived, + ) + + occasion_time_event_blocks: list[TimeEventFullDaysBlock] = [] + if include_occasion_time_events: + occasion_time_event_blocks = await uow.get( + TimeEventFullDaysBlockRepository + ).find_for_owner( + [ + EntityLink.std(NamedEntityTag.OCCASION.value, o.ref_id) + for o in occasions + ], + allow_archived=allow_archived, + ) + + catch_up_tasks: list[InboxTask] = [] + catch_up_tasks_total_cnt = 0 + occasion_tasks: list[InboxTask] = [] + occasion_tasks_total_cnt = 0 + page_size = InboxTaskRepository.PAGE_SIZE + + if include_inbox_tasks: + inbox_task_collection = await uow.get_for( + InboxTaskCollection + ).load_by_parent(workspace_ref_id) + + catch_up_tasks_total_cnt = await uow.get( + InboxTaskRepository + ).count_all_for_owner( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=True, + owner=EntityLink.std(NamedEntityTag.PERSON.value, person.ref_id), + ) + + catch_up_tasks = await uow.get( + InboxTaskRepository + ).find_all_for_owner_created_desc( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=True, + owner=EntityLink.std(NamedEntityTag.PERSON.value, person.ref_id), + retrieve_offset=catch_up_task_retrieve_offset, + retrieve_limit=InboxTaskRepository.PAGE_SIZE, + ) + + occasion_tasks_total_cnt = await uow.get( + InboxTaskRepository + ).count_all_for_owner( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=True, + owner=[ + EntityLink.std(NamedEntityTag.OCCASION.value, o.ref_id) + for o in occasions + ], + ) + + occasion_tasks = await uow.get( + InboxTaskRepository + ).find_all_for_owner_created_desc( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=True, + owner=[ + EntityLink.std(NamedEntityTag.OCCASION.value, o.ref_id) + for o in occasions + ], + retrieve_offset=occasion_task_retrieve_offset, + retrieve_limit=InboxTaskRepository.PAGE_SIZE, + ) + + all_circle_links = await uow.get_for_record(PersonCircleLink).find_all( + person.prm.ref_id + ) + circle_ref_ids = [ + link.circle_ref_id + for link in all_circle_links + if link.person_ref_id == person.ref_id + ] + + if circle_ref_ids: + circles = await uow.get_for(Circle).find_all_generic( + parent_ref_id=person.prm.ref_id, + allow_archived=False, + ref_id=circle_ref_ids, + ) + else: + circles = [] + + tag_domain = await uow.get_for(TagDomain).load_by_parent(workspace_ref_id) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std(NamedEntityTag.PERSON.value, person.ref_id), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + occasion_tag_links = await uow.get(TagLinkRepository).find_all_generic( + parent_ref_id=tag_domain.ref_id, + allow_archived=False, + owner=[ + EntityLink.std(NamedEntityTag.OCCASION.value, o.ref_id) + for o in occasions + ], + ) + all_occasion_tag_ref_ids: list[EntityId] = [] + for tl in occasion_tag_links: + all_occasion_tag_ref_ids.extend(tl.ref_ids) + if all_occasion_tag_ref_ids: + occasion_tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_domain.ref_id, + allow_archived=False, + ref_id=list(set(all_occasion_tag_ref_ids)), + ) + occasion_tags_by_ref_id_map = {t.ref_id: t for t in occasion_tags} + else: + occasion_tags_by_ref_id_map = {} + + occasion_tags_by_ref_id = { + cast(EntityId, link.owner.ref_id): [ + occasion_tags_by_ref_id_map[rid] + for rid in link.ref_ids + if rid in occasion_tags_by_ref_id_map + ] + for link in occasion_tag_links + } + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std(NamedEntityTag.PERSON.value, person.ref_id), + allow_archived=allow_archived, + ) + + return PersonLoadResult( + person=person, + contact=contact, + occasions=occasions, + occasion_tags_by_ref_id=occasion_tags_by_ref_id, + circle_ref_ids=circle_ref_ids, + circles=circles, + note=note, + occasion_time_event_blocks=occasion_time_event_blocks, + catch_up_tasks=catch_up_tasks, + catch_up_tasks_total_cnt=catch_up_tasks_total_cnt, + catch_up_tasks_page_size=page_size, + occasion_tasks=occasion_tasks, + occasion_tasks_total_cnt=occasion_tasks_total_cnt, + occasion_tasks_page_size=page_size, + tags=tags, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/prm/sub/person/sub/occasion/components/stack.tsx b/src/core/jupiter/core/prm/sub/person/sub/occasion/components/stack.tsx index f3c648e9a..bba27d200 100644 --- a/src/core/jupiter/core/prm/sub/person/sub/occasion/components/stack.tsx +++ b/src/core/jupiter/core/prm/sub/person/sub/occasion/components/stack.tsx @@ -1,6 +1,10 @@ import { Tag, type Occasion } from "@jupiter/webapi-client"; -import { EntityCard, EntityLink } from "#/core/infra/component/entity-card"; +import { + EntityCard, + EntityFakeLink, + EntityLink, +} from "#/core/infra/component/entity-card"; import { EntityNameComponent } from "#/core/common/component/entity-name"; import { EntityStack } from "#/core/infra/component/entity-stack"; import { BirthdayTag } from "#/core/common/component/birthday-tag"; @@ -10,28 +14,40 @@ import { TagTag } from "#/core/common/sub/tags/component/tag-tag"; interface OccasionStackProps { occasions: Array<Occasion>; occasionTagsByRefId: Record<string, Array<Tag>>; + linksEnabled?: boolean; } export function OccasionStack(props: OccasionStackProps) { const sortedOccasions = sortOccasionsNaturally(props.occasions); + const linksEnabled = props.linksEnabled ?? true; return ( <EntityStack> {sortedOccasions.map((occasion) => { + const content = ( + <> + <EntityNameComponent name={occasion.name} /> + <BirthdayTag label={""} birthday={occasion.date} /> + {props.occasionTagsByRefId[occasion.ref_id]?.map((tag) => ( + <TagTag key={tag.ref_id} tag={tag} /> + ))} + </> + ); + return ( <EntityCard key={`occasion-${occasion.ref_id}`} entityId={`occasion-${occasion.ref_id}`} > - <EntityLink - to={`/app/workspace/prm/persons/${occasion.person_ref_id}/occasions/${occasion.ref_id}`} - > - <EntityNameComponent name={occasion.name} /> - <BirthdayTag label={""} birthday={occasion.date} /> - {props.occasionTagsByRefId[occasion.ref_id]?.map((tag) => ( - <TagTag key={tag.ref_id} tag={tag} /> - ))} - </EntityLink> + {linksEnabled ? ( + <EntityLink + to={`/app/workspace/prm/persons/${occasion.person_ref_id}/occasions/${occasion.ref_id}`} + > + {content} + </EntityLink> + ) : ( + <EntityFakeLink>{content}</EntityFakeLink> + )} </EntityCard> ); })} diff --git a/src/core/jupiter/core/prm/sub/person/use_case/load.py b/src/core/jupiter/core/prm/sub/person/use_case/load.py index 4092eba40..e44a8e96d 100644 --- a/src/core/jupiter/core/prm/sub/person/use_case/load.py +++ b/src/core/jupiter/core/prm/sub/person/use_case/load.py @@ -1,47 +1,19 @@ """Use case for loading a person.""" -from typing import cast - -from jupiter.core.common.sub.contacts.sub.contact.root import Contact -from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository -from jupiter.core.common.sub.inbox_tasks.collection import ( - InboxTaskCollection, -) -from jupiter.core.common.sub.inbox_tasks.root import ( - InboxTask, - InboxTaskRepository, -) -from jupiter.core.common.sub.notes.root import Note, NoteRepository -from jupiter.core.common.sub.tags.root import TagDomain -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository -from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( - TimeEventFullDaysBlock, - TimeEventFullDaysBlockRepository, -) from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.core.prm.sub.person.root import Person -from jupiter.core.prm.sub.person.sub.occasion.root import Occasion -from jupiter.core.prm.sub.person_circle_links.root import PersonCircleLink +from jupiter.core.prm.sub.person.service.load import PersonLoadResult, PersonLoadService from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink -from jupiter.framework.entity import NoFilter from jupiter.framework.errors import InputValidationError from jupiter.framework.storage.repository import DomainUnitOfWork -from jupiter.framework.use_case import ( - readonly_use_case, -) -from jupiter.framework.use_case_io import ( - UseCaseArgsBase, - UseCaseResultBase, - use_case_args, - use_case_result, -) +from jupiter.framework.use_case import readonly_use_case +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + +__all__ = ["PersonLoadArgs", "PersonLoadResult", "PersonLoadUseCase"] @use_case_args @@ -54,26 +26,6 @@ class PersonLoadArgs(UseCaseArgsBase): occasion_task_retrieve_offset: int | None -@use_case_result -class PersonLoadResult(UseCaseResultBase): - """PersonLoadResult.""" - - person: Person - contact: Contact - circle_ref_ids: list[EntityId] - occasions: list[Occasion] - occasion_tags_by_ref_id: dict[EntityId, list[Tag]] - occasion_time_event_blocks: list[TimeEventFullDaysBlock] - catch_up_tasks: list[InboxTask] - catch_up_tasks_total_cnt: int - catch_up_tasks_page_size: int - occasion_tasks: list[InboxTask] - occasion_tasks_total_cnt: int - occasion_tasks_page_size: int - tags: list[Tag] - note: Note | None - - @readonly_use_case(WorkspaceFeature.PRM) class PersonLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[PersonLoadArgs, PersonLoadResult] @@ -103,148 +55,12 @@ async def _perform_transactional_read( person = await uow.get_for(Person).load_by_id( args.ref_id, allow_archived=allow_archived ) - contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.PERSON.value, person.ref_id), - ) - if contact_link is None or len(contact_link.contacts_ref_ids) == 0: - raise InputValidationError("Person does not have a linked contact") - contact = await uow.get_for(Contact).load_by_id( - contact_link.contacts_ref_ids[0] - ) - - occasions = await uow.get_for(Occasion).find_all_generic( - parent_ref_id=person.ref_id, - allow_archived=False, - ref_id=NoFilter(), - ) - - note = await uow.get(NoteRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.PERSON.value, person.ref_id), - allow_archived=allow_archived, - ) - - occasion_time_event_blocks = await uow.get( - TimeEventFullDaysBlockRepository - ).find_for_owner( - [ - EntityLink.std(NamedEntityTag.OCCASION.value, o.ref_id) - for o in occasions - ], - allow_archived=allow_archived, - ) - inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( + return await PersonLoadService().do_it( + uow, workspace.ref_id, - ) - - catch_up_tasks_total_cnt = await uow.get( - InboxTaskRepository - ).count_all_for_owner( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=True, - owner=EntityLink.std(NamedEntityTag.PERSON.value, args.ref_id), - ) - - catch_up_tasks = await uow.get( - InboxTaskRepository - ).find_all_for_owner_created_desc( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=True, - owner=EntityLink.std(NamedEntityTag.PERSON.value, args.ref_id), - retrieve_offset=args.catch_up_task_retrieve_offset or 0, - retrieve_limit=InboxTaskRepository.PAGE_SIZE, - ) - - occasion_tasks_total_cnt = await uow.get( - InboxTaskRepository - ).count_all_for_owner( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=True, - owner=[ - EntityLink.std(NamedEntityTag.OCCASION.value, o.ref_id) - for o in occasions - ], - ) - - occasion_tasks = await uow.get( - InboxTaskRepository - ).find_all_for_owner_created_desc( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=True, - owner=[ - EntityLink.std(NamedEntityTag.OCCASION.value, o.ref_id) - for o in occasions - ], - retrieve_offset=args.occasion_task_retrieve_offset or 0, - retrieve_limit=InboxTaskRepository.PAGE_SIZE, - ) - - all_circle_links = await uow.get_for_record(PersonCircleLink).find_all( - person.prm.ref_id - ) - circle_ref_ids = [ - link.circle_ref_id - for link in all_circle_links - if link.person_ref_id == person.ref_id - ] - - tag_domain = await uow.get_for(TagDomain).load_by_parent(workspace.ref_id) - - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.PERSON.value, person.ref_id), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - - occasion_tag_links = await uow.get(TagLinkRepository).find_all_generic( - parent_ref_id=tag_domain.ref_id, - allow_archived=False, - owner=[ - EntityLink.std(NamedEntityTag.OCCASION.value, o.ref_id) - for o in occasions - ], - ) - all_occasion_tag_ref_ids: list[EntityId] = [] - for tl in occasion_tag_links: - all_occasion_tag_ref_ids.extend(tl.ref_ids) - if all_occasion_tag_ref_ids: - occasion_tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_domain.ref_id, - allow_archived=False, - ref_id=list(set(all_occasion_tag_ref_ids)), - ) - occasion_tags_by_ref_id_map = {t.ref_id: t for t in occasion_tags} - else: - occasion_tags_by_ref_id_map = {} - - occasion_tags_by_ref_id = { - cast(EntityId, link.owner.ref_id): [ - occasion_tags_by_ref_id_map[rid] - for rid in link.ref_ids - if rid in occasion_tags_by_ref_id_map - ] - for link in occasion_tag_links - } - - return PersonLoadResult( - person=person, - contact=contact, - occasions=occasions, - occasion_tags_by_ref_id=occasion_tags_by_ref_id, - circle_ref_ids=circle_ref_ids, - note=note, - occasion_time_event_blocks=occasion_time_event_blocks, - catch_up_tasks=catch_up_tasks, - catch_up_tasks_total_cnt=catch_up_tasks_total_cnt, - catch_up_tasks_page_size=InboxTaskRepository.PAGE_SIZE, - occasion_tasks=occasion_tasks, - occasion_tasks_total_cnt=occasion_tasks_total_cnt, - occasion_tasks_page_size=InboxTaskRepository.PAGE_SIZE, - tags=tags, + person, + allow_archived=allow_archived, + catch_up_task_retrieve_offset=args.catch_up_task_retrieve_offset or 0, + occasion_task_retrieve_offset=args.occasion_task_retrieve_offset or 0, ) diff --git a/src/core/jupiter/core/prm/sub/person/use_case/load_public.py b/src/core/jupiter/core/prm/sub/person/use_case/load_public.py new file mode 100644 index 000000000..74aaea6a5 --- /dev/null +++ b/src/core/jupiter/core/prm/sub/person/use_case/load_public.py @@ -0,0 +1,77 @@ +"""Guest readonly use case for loading a published person.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.prm.root import PRM +from jupiter.core.prm.sub.person.root import Person +from jupiter.core.prm.sub.person.service.load import PersonLoadResult, PersonLoadService +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class PersonLoadPublicArgs(UseCaseArgsBase): + """PersonLoadPublic args.""" + + external_id: PublishExternalId + + +class PersonLoadPublicUseCase( + JupiterGuestReadonlyUseCase[PersonLoadPublicArgs, PersonLoadResult] +): + """Load a published person by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: PersonLoadPublicArgs, + ) -> PersonLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.PERSON.value: + raise InputValidationError( + "The publish entity does not refer to a person." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + prm = await uow.get_for(PRM).load_by_parent(publish_domain.workspace.ref_id) + person = await uow.get_for(Person).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if person.parent_ref_id != prm.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace person." + ) + + return await PersonLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + person, + allow_archived=False, + include_inbox_tasks=False, + include_occasion_time_events=False, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/schedule/sub/event_full_days/component/editor.tsx b/src/core/jupiter/core/schedule/sub/event_full_days/component/editor.tsx index a19f172a4..af1484c60 100644 --- a/src/core/jupiter/core/schedule/sub/event_full_days/component/editor.tsx +++ b/src/core/jupiter/core/schedule/sub/event_full_days/component/editor.tsx @@ -14,6 +14,7 @@ import { OutlinedInput, Stack, } from "@mui/material"; + import { entityLinkStd } from "#/core/common/entity-link"; import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; @@ -112,12 +113,7 @@ export function ScheduleEventFullDaysEditor( defaultValue={selectedScheduleStream} /> ) : ( - <OutlinedInput - label="Schedule Stream" - readOnly - disabled - value="—" - /> + <OutlinedInput label="Schedule Stream" readOnly disabled value="—" /> )} <FieldError actionResult={props.actionResult} @@ -225,7 +221,10 @@ export function ScheduleEventFullDaysEditor( } /> - <FieldError actionResult={props.actionResult} fieldName="/duration_days" /> + <FieldError + actionResult={props.actionResult} + fieldName="/duration_days" + /> </FormControl> </Stack> </SectionCard> diff --git a/src/core/jupiter/core/schedule/sub/event_full_days/service/load.py b/src/core/jupiter/core/schedule/sub/event_full_days/service/load.py index 30088d272..04ab00287 100644 --- a/src/core/jupiter/core/schedule/sub/event_full_days/service/load.py +++ b/src/core/jupiter/core/schedule/sub/event_full_days/service/load.py @@ -1,5 +1,6 @@ """Shared service for loading a schedule full days event.""" +from jupiter.core.application.fast_info_repository import ScheduleStreamSummary from jupiter.core.common.sub.contacts.root import ContactDomain from jupiter.core.common.sub.contacts.sub.contact.root import Contact from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository @@ -13,7 +14,6 @@ from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( TimeEventFullDaysBlock, ) -from jupiter.core.application.fast_info_repository import ScheduleStreamSummary from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.core.schedule.sub.event_full_days.root import ScheduleEventFullDays from jupiter.core.schedule.sub.stream.root import ScheduleStream diff --git a/src/core/jupiter/core/schedule/sub/event_in_day/component/editor.tsx b/src/core/jupiter/core/schedule/sub/event_in_day/component/editor.tsx index 8b99476e4..dde21b7ec 100644 --- a/src/core/jupiter/core/schedule/sub/event_in_day/component/editor.tsx +++ b/src/core/jupiter/core/schedule/sub/event_in_day/component/editor.tsx @@ -14,6 +14,7 @@ import { OutlinedInput, Stack, } from "@mui/material"; + import { entityLinkStd } from "#/core/common/entity-link"; import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; @@ -119,12 +120,7 @@ export function ScheduleEventInDayEditor(props: ScheduleEventInDayEditorProps) { defaultValue={selectedScheduleStream} /> ) : ( - <OutlinedInput - label="Schedule Stream" - readOnly - disabled - value="—" - /> + <OutlinedInput label="Schedule Stream" readOnly disabled value="—" /> )} <FieldError actionResult={props.actionResult} @@ -189,7 +185,10 @@ export function ScheduleEventInDayEditor(props: ScheduleEventInDayEditorProps) { onChange={(e) => props.onStartDateChange?.(e.target.value)} /> - <FieldError actionResult={props.actionResult} fieldName="/start_date" /> + <FieldError + actionResult={props.actionResult} + fieldName="/start_date" + /> </FormControl> <FormControl fullWidth> @@ -256,7 +255,10 @@ export function ScheduleEventInDayEditor(props: ScheduleEventInDayEditorProps) { }} /> - <FieldError actionResult={props.actionResult} fieldName="/duration_mins" /> + <FieldError + actionResult={props.actionResult} + fieldName="/duration_mins" + /> </FormControl> </Stack> </SectionCard> diff --git a/src/core/jupiter/core/schedule/sub/event_in_day/service/load.py b/src/core/jupiter/core/schedule/sub/event_in_day/service/load.py index 121793373..d15ca9b26 100644 --- a/src/core/jupiter/core/schedule/sub/event_in_day/service/load.py +++ b/src/core/jupiter/core/schedule/sub/event_in_day/service/load.py @@ -1,5 +1,6 @@ """Shared service for loading a schedule event in day.""" +from jupiter.core.application.fast_info_repository import ScheduleStreamSummary from jupiter.core.common.sub.contacts.root import ContactDomain from jupiter.core.common.sub.contacts.sub.contact.root import Contact from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository @@ -13,7 +14,6 @@ from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( TimeEventInDayBlock, ) -from jupiter.core.application.fast_info_repository import ScheduleStreamSummary from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.core.schedule.sub.event_in_day.root import ScheduleEventInDay from jupiter.core.schedule.sub.stream.root import ScheduleStream diff --git a/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public.py b/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public.py index 12f144a45..b0cae9d3d 100644 --- a/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public.py +++ b/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public.py @@ -49,7 +49,10 @@ async def _execute( "The publish entity is not active and cannot be loaded." ) - if publish_entity.owner.the_type != NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value: + if ( + publish_entity.owner.the_type + != NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value + ): raise InputValidationError( "The publish entity does not refer to a schedule event in day." ) diff --git a/src/core/jupiter/core/smart_lists/sub/item/component/editor.tsx b/src/core/jupiter/core/smart_lists/sub/item/component/editor.tsx index aa1424f13..e82d984d6 100644 --- a/src/core/jupiter/core/smart_lists/sub/item/component/editor.tsx +++ b/src/core/jupiter/core/smart_lists/sub/item/component/editor.tsx @@ -8,6 +8,7 @@ import { Stack, Switch, } from "@mui/material"; + import { entityLinkStd } from "#/core/common/entity-link"; import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; diff --git a/src/core/jupiter/core/time_plans/component/editor.tsx b/src/core/jupiter/core/time_plans/component/editor.tsx index 671f5a5df..c83217a93 100644 --- a/src/core/jupiter/core/time_plans/component/editor.tsx +++ b/src/core/jupiter/core/time_plans/component/editor.tsx @@ -12,6 +12,7 @@ import type { } from "@jupiter/webapi-client"; import { NamedEntityTag, WorkspaceFeature } from "@jupiter/webapi-client"; import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; + import { aDateToDate } from "#/core/common/adate"; import { entityLinkStd } from "#/core/common/entity-link"; import { PeriodSelect } from "#/core/common/component/period-select"; @@ -52,8 +53,7 @@ interface TimePlanEditorProps { export function TimePlanEditor(props: TimePlanEditorProps) { const isBigScreen = useBigScreen(); const { timePlan, tags, allTags, aspects, chapters, goals } = props; - const timeConfigEditable = - props.inputsEnabled && props.corePropertyEditable; + const timeConfigEditable = props.inputsEnabled && props.corePropertyEditable; const changeTimeConfigIntent = props.corePropertyEditable ? "change-time-config" : "change-time-config-for-generated"; @@ -80,11 +80,7 @@ export function TimePlanEditor(props: TimePlanEditorProps) { ) : undefined } > - <Stack - direction={isBigScreen ? "row" : "column"} - spacing={2} - useFlexGap - > + <Stack direction={isBigScreen ? "row" : "column"} spacing={2} useFlexGap> <FormControl fullWidth={!isBigScreen}> <InputLabel id="rightNow" shrink margin="dense"> The Date diff --git a/src/core/jupiter/core/vacations/component/editor.tsx b/src/core/jupiter/core/vacations/component/editor.tsx index 611e4efc9..bf72fdec6 100644 --- a/src/core/jupiter/core/vacations/component/editor.tsx +++ b/src/core/jupiter/core/vacations/component/editor.tsx @@ -1,6 +1,7 @@ import type { Contact, Tag, Vacation } from "@jupiter/webapi-client"; import { NamedEntityTag } from "@jupiter/webapi-client"; import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; + import { aDateToDate } from "#/core/common/adate"; import { entityLinkStd } from "#/core/common/entity-link"; import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx index 5287e39d0..984d7506d 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -33,6 +33,10 @@ function publishedEntityLocation(externalId: string, owner: string): string { return `/app/public/published/smart-list-item/${externalId}`; case NamedEntityTag.METRIC_ENTRY: return `/app/public/published/metric-entry/${externalId}`; + case NamedEntityTag.DOC: + return `/app/public/published/doc/${externalId}`; + case NamedEntityTag.PERSON: + return `/app/public/published/person/${externalId}`; default: throw new Response(ReasonPhrases.NOT_FOUND, { status: StatusCodes.NOT_FOUND, diff --git a/src/webui/app/routes/app/public/published/doc/$externalId.tsx b/src/webui/app/routes/app/public/published/doc/$externalId.tsx new file mode 100644 index 000000000..a7b4c0a34 --- /dev/null +++ b/src/webui/app/routes/app/public/published/doc/$externalId.tsx @@ -0,0 +1,93 @@ +import { ApiError, NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { entityLinkStd } from "@jupiter/core/common/entity-link"; +import { TagsEditor } from "@jupiter/core/common/sub/tags/component/tags-editor"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { DocEditor } from "@jupiter/core/docs/component/editor"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.docs.docLoadPublic({ + external_id: externalId, + }); + + return json({ + doc: result.doc, + note: result.note, + tags: result.tags ?? [], + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedDoc() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const { doc, note, tags } = loaderData; + + return ( + <LeafPanel + key={`published-doc-${doc.ref_id}`} + fakeKey={`published-doc-${doc.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <SectionCard title="Doc"> + <DocEditor + initialDoc={doc} + initialNote={note} + inputsEnabled={false} + parentDirRefId={doc.parent_dir_ref_id} + rightOfName={ + <TagsEditor + name="tags" + allTags={tags} + defaultValue={tags.map((tag) => tag.ref_id)} + inputsEnabled={false} + owner={entityLinkStd(NamedEntityTag.DOC, doc.ref_id)} + /> + } + /> + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => `Could not find published doc ${params.externalId}!`, + error: (params) => + `There was an error loading published doc ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/public/published/person/$externalId.tsx b/src/webui/app/routes/app/public/published/person/$externalId.tsx new file mode 100644 index 000000000..8c5eb77c4 --- /dev/null +++ b/src/webui/app/routes/app/public/published/person/$externalId.tsx @@ -0,0 +1,125 @@ +import { ApiError } from "@jupiter/webapi-client"; +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { PersonEditor } from "@jupiter/core/prm/sub/person/component/editor"; +import { OccasionStack } from "@jupiter/core/prm/sub/person/sub/occasion/components/stack"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.prm.personLoadPublic({ + external_id: externalId, + }); + + return json({ + person: result.person, + contact: result.contact, + tags: result.tags ?? [], + circles: result.circles ?? [], + circleRefIds: result.circle_ref_ids ?? [], + occasions: result.occasions ?? [], + occasionTagsByRefId: result.occasion_tags_by_ref_id ?? {}, + note: result.note ?? null, + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedPerson() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const { + person, + contact, + tags, + circles, + circleRefIds, + occasions, + occasionTagsByRefId, + note, + } = loaderData; + + return ( + <LeafPanel + key={`published-person-${person.ref_id}`} + fakeKey={`published-person-${person.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <SectionCard title="Properties"> + <PersonEditor + person={person} + contact={contact} + tags={tags} + allTags={tags} + allCircles={circles} + circleRefIds={circleRefIds} + maxCirclesPerPerson={3} + inputsEnabled={false} + topLevelInfo={topLevelInfo} + /> + </SectionCard> + + <SectionCard title="Occasions"> + <OccasionStack + occasions={occasions} + occasionTagsByRefId={occasionTagsByRefId} + linksEnabled={false} + /> + </SectionCard> + + <SectionCard title="Note"> + {note ? ( + <EntityNoteEditor initialNote={note} inputsEnabled={false} /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => `Could not find published person ${params.externalId}!`, + error: (params) => + `There was an error loading published person ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx b/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx index 8bb3a18a8..175029f76 100644 --- a/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx @@ -11,10 +11,7 @@ import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext, useMemo } from "react"; import { z } from "zod"; import { parseParams } from "zodix"; -import { - isTimePlanActivityBigPlanTarget, - isTimePlanActivityInboxTaskTarget, -} from "@jupiter/core/time_plans/sub/activity/target-wire"; +import { isTimePlanActivityBigPlanTarget } from "@jupiter/core/time_plans/sub/activity/target-wire"; import { filterActivityByFeasabilityWithParents } from "@jupiter/core/time_plans/sub/activity/root"; import { entityLinkRefIdFromWire } from "@jupiter/core/common/sub/inbox_tasks/parent-link-namespace"; import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; @@ -92,7 +89,8 @@ export default function PublishedTimePlan() { } = loaderData; const targetInboxTasksByRefId = useMemo( - () => new Map<string, InboxTask>(targetInboxTasks.map((it) => [it.ref_id, it])), + () => + new Map<string, InboxTask>(targetInboxTasks.map((it) => [it.ref_id, it])), [targetInboxTasks], ); const targetBigPlansByRefId = useMemo( diff --git a/src/webui/app/routes/app/public/published/vacation/$externalId.tsx b/src/webui/app/routes/app/public/published/vacation/$externalId.tsx index 458ac5a71..bd52c6116 100644 --- a/src/webui/app/routes/app/public/published/vacation/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/vacation/$externalId.tsx @@ -6,7 +6,6 @@ import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseParams } from "zodix"; -import { TimeEventFullDaysBlockStack } from "@jupiter/core/common/sub/time_events/sub/full_days_block/component/stack"; import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; @@ -58,15 +57,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { export default function PublishedVacation() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); - const { vacation, note, timeEventBlock, tags, contacts } = loaderData; - - const timeEventBlockEntry = { - time_event: timeEventBlock, - entry: { - vacation: vacation, - time_event: timeEventBlock, - }, - }; + const { vacation, note, tags, contacts } = loaderData; return ( <LeafPanel diff --git a/src/webui/app/routes/app/workspace/core/publish.tsx b/src/webui/app/routes/app/workspace/core/publish.tsx new file mode 100644 index 000000000..bcce9d805 --- /dev/null +++ b/src/webui/app/routes/app/workspace/core/publish.tsx @@ -0,0 +1,198 @@ +import type { PublishEntity } from "@jupiter/webapi-client"; +import { + DocsHelpSubject, + NamedEntityTag, + PublishEntityStatus, +} from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import type { ShouldRevalidateFunction } from "@remix-run/react"; +import { Outlet } from "@remix-run/react"; +import { AnimatePresence } from "framer-motion"; +import { Button, Box, Stack, Typography } from "@mui/material"; +import { useContext, useMemo, useState } from "react"; +import { parseEntityLinkStd } from "@jupiter/core/common/entity-link"; +import { + EntityCard, + EntityLink, +} from "@jupiter/core/infra/component/entity-card"; +import { EntityNoNothingCard } from "@jupiter/core/infra/component/entity-no-nothing-card"; +import { EntityStack } from "@jupiter/core/infra/component/entity-stack"; +import { makeTrunkErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { NestingAwareBlock } from "@jupiter/core/infra/component/layout/nesting-aware-block"; +import { TrunkPanel } from "@jupiter/core/infra/component/layout/trunk-panel"; +import { + DisplayType, + useTrunkNeedsToShowLeaf, +} from "@jupiter/core/infra/component/use-nested-entities"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { + FilterManyOptions, + SectionActions, +} from "@jupiter/core/infra/component/section-actions"; +import { SlimChip } from "@jupiter/core/infra/component/chips"; +import { PublishOwnerTypeChip } from "#/core/common/sub/publish/components/publish-owner-type-chip"; +import { publishOwnerEntityTagName } from "#/core/common/sub/publish/publish-owner-type-name"; +import { ServicePropertiesContext } from "#/core/config-client"; + +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; +import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; +import { getLoggedInApiClient } from "~/api-clients.server"; + +export const handle = { + displayType: DisplayType.TRUNK, +}; + +export async function loader({ request }: LoaderFunctionArgs) { + const apiClient = await getLoggedInApiClient(request); + + const [findResult, settingsResult] = await Promise.all([ + apiClient.publish.publishEntityFind({ + allow_archived: false, + }), + apiClient.publish.publishEntityLoadSettings({}), + ]); + + const publishOwnerFilterTags = + settingsResult.allowed_publish_owner_entity_types.map( + (s) => s as NamedEntityTag, + ); + + return json({ + publishEntities: findResult.publish_entities as Array<PublishEntity>, + publishOwnerFilterTags, + }); +} + +export const shouldRevalidate: ShouldRevalidateFunction = + standardShouldRevalidate; + +export default function PublishEntities() { + const { publishEntities, publishOwnerFilterTags } = + useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const serviceProperties = useContext(ServicePropertiesContext); + const shouldShowALeafToo = useTrunkNeedsToShowLeaf(); + + const [selectedOwnerTypes, setSelectedOwnerTypes] = useState< + NamedEntityTag[] + >([]); + + const ownerTypeOptions = useMemo( + () => + publishOwnerFilterTags.map((tag) => ({ + value: tag, + text: publishOwnerEntityTagName(tag), + })), + [publishOwnerFilterTags], + ); + + const filteredPublishEntities = useMemo(() => { + const sorted = [...publishEntities].sort((a, b) => { + const ta = parseEntityLinkStd(a.owner).theType; + const tb = parseEntityLinkStd(b.owner).theType; + if (ta < tb) { + return -1; + } + if (ta > tb) { + return 1; + } + return a.external_id.localeCompare(b.external_id); + }); + + if (selectedOwnerTypes.length === 0) { + return sorted; + } + + return sorted.filter((publishEntity) => { + const { theType } = parseEntityLinkStd(publishEntity.owner); + return selectedOwnerTypes.some((t) => t === theType); + }); + }, [publishEntities, selectedOwnerTypes]); + + return ( + <TrunkPanel + key={"core/publish"} + returnLocation="/app/workspace" + actions={ + <SectionActions + id="core-publish-actions" + topLevelInfo={topLevelInfo} + inputsEnabled={true} + actions={[ + FilterManyOptions( + "Entity type", + ownerTypeOptions, + setSelectedOwnerTypes, + ), + ]} + /> + } + > + <NestingAwareBlock shouldHide={shouldShowALeafToo}> + {filteredPublishEntities.length === 0 && ( + <EntityNoNothingCard + title="No Shared Entities" + message="There are no shared entities to show. Publish an entity from its detail view to share a read-only link." + helpSubject={DocsHelpSubject.ROOT} + /> + )} + + <EntityStack> + {filteredPublishEntities.map((publishEntity) => { + const publicUrl = `${serviceProperties.webUiUrl}/app/public/published/${publishEntity.external_id}`; + const isActive = + publishEntity.status === PublishEntityStatus.ACTIVE; + + return ( + <EntityCard + entityId={`publish-${publishEntity.ref_id}`} + key={`publish-${publishEntity.ref_id}`} + > + <Stack + direction="row" + alignItems="center" + spacing={1} + sx={{ width: "100%", paddingRight: "1rem" }} + > + <Box sx={{ flex: 1, minWidth: 0 }}> + <EntityLink + to={`/app/workspace/core/publish/${publishEntity.ref_id}`} + > + <PublishOwnerTypeChip owner={publishEntity.owner} /> + <SlimChip label={publishEntity.status} color="default" /> + <Typography variant="body2" color="text.secondary" noWrap> + {publicUrl} + </Typography> + </EntityLink> + </Box> + <Button + component="a" + href={publicUrl} + target="_blank" + rel="noreferrer" + variant="outlined" + size="small" + disabled={!isActive} + onClick={(event) => event.stopPropagation()} + > + View + </Button> + </Stack> + </EntityCard> + ); + })} + </EntityStack> + </NestingAwareBlock> + + <AnimatePresence mode="wait" initial={false}> + <Outlet /> + </AnimatePresence> + </TrunkPanel> + ); +} + +export const ErrorBoundary = makeTrunkErrorBoundary("/app/workspace", { + error: () => + `There was an error loading the shared entities! Please try again!`, +}); diff --git a/src/webui/app/routes/app/workspace/core/publish/$publishEntityId.tsx b/src/webui/app/routes/app/workspace/core/publish/$publishEntityId.tsx new file mode 100644 index 000000000..37699d637 --- /dev/null +++ b/src/webui/app/routes/app/workspace/core/publish/$publishEntityId.tsx @@ -0,0 +1,156 @@ +import { + ApiError, + NamedEntityTag, + type PublishEntity, +} from "@jupiter/webapi-client"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; +import { json, redirect } from "@remix-run/node"; +import type { ShouldRevalidateFunction } from "@remix-run/react"; +import { useActionData, useNavigation } from "@remix-run/react"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext } from "react"; +import { z } from "zod"; +import { parseForm, parseParams } from "zodix"; +import { parseEntityLinkStd } from "@jupiter/core/common/entity-link"; +import { PublishPanel } from "#/core/common/sub/publish/components/publish-panel"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { GlobalError } from "@jupiter/core/infra/component/errors"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; + +import { getLoggedInApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; +import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; + +const ParamsSchema = z.object({ + publishEntityId: z.string(), +}); + +const UpdateFormSchema = z.discriminatedUnion("intent", [ + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), +]); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const apiClient = await getLoggedInApiClient(request); + const { publishEntityId } = parseParams(params, ParamsSchema); + + try { + const result = await apiClient.publish.publishEntityLoad({ + ref_id: publishEntityId, + allow_archived: true, + }); + + return json({ + publishEntity: result.publish_entity as PublishEntity, + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export async function action({ request, params }: ActionFunctionArgs) { + const apiClient = await getLoggedInApiClient(request); + const { publishEntityId } = parseParams(params, ParamsSchema); + const form = await parseForm(request, UpdateFormSchema); + + try { + switch (form.intent) { + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/core/publish/${publishEntityId}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/core/publish/${publishEntityId}`); + } + + default: + throw new Response("Bad Intent", { status: 500 }); + } + } catch (error) { + if ( + error instanceof ApiError && + error.status === StatusCodes.UNPROCESSABLE_ENTITY + ) { + return json(validationErrorToUIErrorInfo(error.body)); + } + + if (error instanceof ApiError && error.status === StatusCodes.CONFLICT) { + return json(validationErrorToUIErrorInfo(error.body)); + } + + throw error; + } +} + +export const shouldRevalidate: ShouldRevalidateFunction = + standardShouldRevalidate; + +export default function PublishEntityView() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const actionData = useActionData<typeof action>(); + const navigation = useNavigation(); + const topLevelInfo = useContext(TopLevelInfoContext); + + const publishEntity = loaderData.publishEntity; + const { theType, refId } = parseEntityLinkStd(publishEntity.owner); + const entityType = theType as NamedEntityTag; + const inputsEnabled = navigation.state === "idle" && !publishEntity.archived; + + return ( + <LeafPanel + key={`publish-${publishEntity.ref_id}`} + fakeKey={`publish-${publishEntity.ref_id}`} + inputsEnabled={inputsEnabled} + returnLocation="/app/workspace/core/publish" + > + <GlobalError actionResult={actionData} /> + <PublishPanel + entityType={entityType} + entityRefId={refId} + topLevelInfo={topLevelInfo} + inputsEnabled={inputsEnabled} + publishEntity={publishEntity} + /> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary( + "/app/workspace/core/publish", + ParamsSchema, + { + notFound: (params) => + `Could not find shared entity ${params.publishEntityId}!`, + error: (params) => + `There was an error loading shared entity ${params.publishEntityId}! Please try again!`, + }, +); diff --git a/src/webui/app/routes/app/workspace/docs/$dirId/doc/$docId.tsx b/src/webui/app/routes/app/workspace/docs/$dirId/doc/$docId.tsx index 4bfa3d070..68bf3f6c2 100644 --- a/src/webui/app/routes/app/workspace/docs/$dirId/doc/$docId.tsx +++ b/src/webui/app/routes/app/workspace/docs/$dirId/doc/$docId.tsx @@ -35,6 +35,18 @@ const ParamsSchema = z.object({ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("archive") }), z.object({ intent: z.literal("remove") }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -66,6 +78,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { doc: result.doc, note: result.note, tags: result.tags, + publishEntity: result.publish_entity ?? null, allTags: allTags.tags, dirId, }); @@ -102,6 +115,30 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/docs/${dirId}`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/docs/${dirId}/doc/${docId}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/docs/${dirId}/doc/${docId}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/docs/${dirId}/doc/${docId}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -137,6 +174,8 @@ export default function DocInFolder() { showArchiveAndRemoveButton inputsEnabled={inputsEnabled} entityArchived={loaderData.doc.archived} + publishable + publishEntity={loaderData.publishEntity ?? undefined} returnLocation={`/app/workspace/docs/${loaderData.dirId}`} initialExpansionState={LeafPanelExpansionState.FULL} shouldShowALeaflet={shouldShowALeaflet} diff --git a/src/webui/app/routes/app/workspace/prm/persons/$id.tsx b/src/webui/app/routes/app/workspace/prm/persons/$id.tsx index 102288976..6f869c5b5 100644 --- a/src/webui/app/routes/app/workspace/prm/persons/$id.tsx +++ b/src/webui/app/routes/app/workspace/prm/persons/$id.tsx @@ -9,7 +9,6 @@ import { WorkspaceFeature, Tag, } from "@jupiter/webapi-client"; -import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; import { ActionFunctionArgs, LoaderFunctionArgs, @@ -27,20 +26,15 @@ import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseForm, parseParams, parseQuery } from "zodix"; -import { - entityLinkStd, - parseEntityLinkStd, -} from "@jupiter/core/common/entity-link"; +import { parseEntityLinkStd } from "@jupiter/core/common/entity-link"; import { isWorkspaceFeatureAvailable } from "@jupiter/core/workspaces/root"; import { sortBirthdayTimeEventsNaturally as sortOccasionTimeEventsNaturally } from "@jupiter/core/common/sub/time_events/time-event"; import { sortInboxTasksNaturally } from "#/core/common/sub/inbox_tasks/root"; import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; import { InboxTaskStack } from "@jupiter/core/common/sub/inbox_tasks/component/stack"; import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; -import { FieldError, GlobalError } from "@jupiter/core/infra/component/errors"; +import { GlobalError } from "@jupiter/core/infra/component/errors"; import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; -import { RecurringTaskGenParamsBlock } from "@jupiter/core/common/component/recurring-task-gen-params-block"; -import { StandardDivider } from "@jupiter/core/infra/component/standard-divider"; import { TimeEventFullDaysBlockStack } from "@jupiter/core/common/sub/time_events/sub/full_days_block/component/stack"; import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { @@ -54,12 +48,10 @@ import { NavSingle, } from "@jupiter/core/infra/component/section-actions"; import { SectionCard } from "@jupiter/core/infra/component/section-card"; -import { CircleMultiSelect } from "@jupiter/core/prm/sub/circle/components/multi-select"; +import { PersonEditor } from "@jupiter/core/prm/sub/person/component/editor"; import { OccasionStack } from "@jupiter/core/prm/sub/person/sub/occasion/components/stack"; import { AnimatePresence } from "framer-motion"; import { NestingAwareBlock } from "#/core/infra/component/layout/nesting-aware-block"; -import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; -import { useBigScreen } from "@jupiter/core/infra/component/use-big-screen"; import { noteStdOwner } from "#/core/common/sub/notes/note-std-owner"; import { fixSelectOutputEntityId, @@ -112,6 +104,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -156,6 +160,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { occasionTasksPageSize: result.occasion_tasks_page_size, tags: result.tags, note: result.note, + publishEntity: result.publish_entity, occasionTimeEventBlocks: result.occasion_time_event_blocks, allTags: allTags.tags as Array<Tag>, }); @@ -287,6 +292,30 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/prm/persons`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/prm/persons/${id}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/prm/persons/${id}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/prm/persons/${id}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -314,11 +343,9 @@ export default function Person() { const actionData = useActionData<typeof action>(); const navigation = useNavigation(); const topLevelInfo = useContext(TopLevelInfoContext); - const isBigScreen = useBigScreen(); const shouldShowALeaflet = useLeafNeedsToShowLeaflet(); const person = loaderData.person; - const personName = loaderData.contact.name; const allOccasionsByRefId = new Map( loaderData.occasions.map((o) => [o.ref_id, o]), ); @@ -391,6 +418,8 @@ export default function Person() { entityArchived={person.archived} returnLocation="/app/workspace/prm/persons" shouldShowALeaflet={shouldShowALeaflet} + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <NestingAwareBlock shouldHide={shouldShowALeaflet}> <GlobalError actionResult={actionData} /> @@ -416,57 +445,17 @@ export default function Person() { /> } > - <Stack direction={isBigScreen ? "row" : "column"} spacing={1}> - <FormControl fullWidth sx={{ flexGrow: 3 }}> - <InputLabel id="name">Name</InputLabel> - <OutlinedInput - label="Name" - name="name" - readOnly={!inputsEnabled} - defaultValue={personName} - /> - <FieldError actionResult={actionData} fieldName="/name" /> - </FormControl> - - <FormControl fullWidth sx={{ flexGrow: 2 }}> - <TagsEditor - name="tags" - label={null} - aloneOnLine={!isBigScreen} - allTags={loaderData.allTags} - defaultValue={loaderData.tags.map((tag) => tag.ref_id)} - inputsEnabled={inputsEnabled} - owner={entityLinkStd(NamedEntityTag.PERSON, person.ref_id)} - /> - </FormControl> - </Stack> - - <CircleMultiSelect - name="circleRefIds" - label="Circles" - inputsEnabled={inputsEnabled} - disabled={false} + <PersonEditor + person={person} + contact={loaderData.contact} + tags={loaderData.tags} + allTags={loaderData.allTags} allCircles={loaderData.allCircles} - defaultValue={loaderData.circleRefIds} - maxSelections={loaderData.maxCirclesPerPerson} - /> - <FieldError actionResult={actionData} fieldName="/circle_ref_ids" /> - - <StandardDivider title="Catch Up" size="small" /> - - <RecurringTaskGenParamsBlock - namePrefix="catchUp" - fieldsPrefix="catch_up" - allowNonePeriod - period={person.catch_up_params?.period ?? "none"} - eisen={person.catch_up_params?.eisen} - difficulty={person.catch_up_params?.difficulty} - actionableFromDay={person.catch_up_params?.actionable_from_day} - actionableFromMonth={person.catch_up_params?.actionable_from_month} - dueAtDay={person.catch_up_params?.due_at_day} - dueAtMonth={person.catch_up_params?.due_at_month} + circleRefIds={loaderData.circleRefIds} + maxCirclesPerPerson={loaderData.maxCirclesPerPerson} inputsEnabled={inputsEnabled} - actionData={actionData} + topLevelInfo={topLevelInfo} + actionResult={actionData} /> </SectionCard> diff --git a/src/webui/app/routes/app/workspace/time-plans/$id.tsx b/src/webui/app/routes/app/workspace/time-plans/$id.tsx index 0c0113443..13606566b 100644 --- a/src/webui/app/routes/app/workspace/time-plans/$id.tsx +++ b/src/webui/app/routes/app/workspace/time-plans/$id.tsx @@ -84,7 +84,6 @@ import { TimeAndEffortView } from "@jupiter/core/time_plans/component/time-and-e import { FeasabilityView } from "@jupiter/core/time_plans/component/feasaibility-view"; import { computeTimeAndEffortSummary } from "@jupiter/core/time_plans/time-and-effort-summary"; import { - ActionSingle, FilterFewOptionsSpread, FilterManyOptions, NavMultipleCompact, @@ -644,10 +643,10 @@ export default function TimePlanView() { chapters={loaderData.chapters} goals={loaderData.goals} lifePlan={loaderData.lifePlan} - allAspects={loaderData.allAspects} - allChapters={loaderData.allChapters} - allGoals={loaderData.allGoals} - allMilestones={loaderData.allMilestones} + allAspects={loaderData.allAspects ?? undefined} + allChapters={loaderData.allChapters ?? undefined} + allGoals={loaderData.allGoals ?? undefined} + allMilestones={loaderData.allMilestones ?? undefined} inputsEnabled={inputsEnabled} corePropertyEditable={corePropertyEditable} topLevelInfo={topLevelInfo} diff --git a/src/webui/app/routes/app/workspace/todos/$id.tsx b/src/webui/app/routes/app/workspace/todos/$id.tsx index 6af7a4488..09655b80c 100644 --- a/src/webui/app/routes/app/workspace/todos/$id.tsx +++ b/src/webui/app/routes/app/workspace/todos/$id.tsx @@ -394,7 +394,7 @@ export default function TodoTask() { entityArchived={loaderData.todoTask.archived} returnLocation="/app/workspace/todos" publishable - publishEntity={loaderData.publishEntity} + publishEntity={loaderData.publishEntity ?? undefined} > <GlobalError actionResult={actionData} /> <TodoTaskPropertiesEditor From 2bb15ff7d079d4d53cd05978f3144c55cb4d554a Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Mon, 8 Jun 2026 10:29:30 +0300 Subject: [PATCH 08/21] Generation for habits and chores --- .../api/chores/chore_load_public.py | 212 ++++++++++++++++++ .../api/habits/habit_load_public.py | 212 ++++++++++++++++++ .../jupiter_webapi_client/models/__init__.py | 4 + .../models/chore_load_public_args.py | 84 +++++++ .../models/chore_load_result.py | 33 +++ .../models/habit_load_public_args.py | 128 +++++++++++ .../models/habit_load_result.py | 33 +++ gen/ts/webapi-client/gen/index.ts | 2 + .../gen/models/ChoreLoadPublicArgs.ts | 13 ++ .../gen/models/ChoreLoadResult.ts | 2 + .../gen/models/HabitLoadPublicArgs.ts | 16 ++ .../gen/models/HabitLoadResult.ts | 2 + .../gen/services/ChoresService.ts | 29 +++ .../gen/services/HabitsService.ts | 29 +++ itests/webui/entities/chores.test.py | 34 ++- itests/webui/entities/habits.test.py | 34 ++- .../jupiter/core/chores/component/editor.tsx | 165 ++++++++++++++ src/core/jupiter/core/chores/root.py | 4 + .../jupiter/core/chores/service/__init__.py | 2 +- src/core/jupiter/core/chores/service/load.py | 160 +++++++++++++ src/core/jupiter/core/chores/use_case/load.py | 129 +---------- .../core/chores/use_case/load_public.py | 85 +++++++ .../common/sub/inbox_tasks/component/card.tsx | 37 +-- .../sub/inbox_tasks/component/stack.tsx | 2 + .../common/sub/publish/sub/entity/root.py | 4 +- .../jupiter/core/habits/component/editor.tsx | 178 +++++++++++++++ src/core/jupiter/core/habits/root.py | 4 + .../jupiter/core/habits/service/__init__.py | 2 +- src/core/jupiter/core/habits/service/load.py | 192 ++++++++++++++++ src/core/jupiter/core/habits/use_case/load.py | 154 +------------ .../core/habits/use_case/load_public.py | 99 ++++++++ .../app/public/published/$externalId.tsx | 4 + .../public/published/chore/$externalId.tsx | 149 ++++++++++++ .../public/published/habit/$externalId.tsx | 168 ++++++++++++++ .../app/routes/app/workspace/chores/$id.tsx | 43 ++++ .../app/routes/app/workspace/habits/$id.tsx | 43 ++++ 36 files changed, 2206 insertions(+), 285 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/chores/chore_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/habits/habit_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/chore_load_public_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/habit_load_public_args.py create mode 100644 gen/ts/webapi-client/gen/models/ChoreLoadPublicArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/HabitLoadPublicArgs.ts create mode 100644 src/core/jupiter/core/chores/component/editor.tsx create mode 100644 src/core/jupiter/core/chores/service/load.py create mode 100644 src/core/jupiter/core/chores/use_case/load_public.py create mode 100644 src/core/jupiter/core/habits/component/editor.tsx create mode 100644 src/core/jupiter/core/habits/service/load.py create mode 100644 src/core/jupiter/core/habits/use_case/load_public.py create mode 100644 src/webui/app/routes/app/public/published/chore/$externalId.tsx create mode 100644 src/webui/app/routes/app/public/published/habit/$externalId.tsx diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/chores/chore_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/chores/chore_load_public.py new file mode 100644 index 000000000..55ac740ca --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/chores/chore_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.chore_load_public_args import ChoreLoadPublicArgs +from ...models.chore_load_result import ChoreLoadResult +from ...models.error_response import ErrorResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: ChoreLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/chore-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ChoreLoadResult | ErrorResponse | None: + if response.status_code == 200: + response_200 = ChoreLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ChoreLoadResult | ErrorResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: ChoreLoadPublicArgs | Unset = UNSET, +) -> Response[ChoreLoadResult | ErrorResponse]: + """Load a published chore by publish external id. + + Args: + body (ChoreLoadPublicArgs | Unset): ChoreLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ChoreLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: ChoreLoadPublicArgs | Unset = UNSET, +) -> ChoreLoadResult | ErrorResponse | None: + """Load a published chore by publish external id. + + Args: + body (ChoreLoadPublicArgs | Unset): ChoreLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ChoreLoadResult | ErrorResponse + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: ChoreLoadPublicArgs | Unset = UNSET, +) -> Response[ChoreLoadResult | ErrorResponse]: + """Load a published chore by publish external id. + + Args: + body (ChoreLoadPublicArgs | Unset): ChoreLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ChoreLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: ChoreLoadPublicArgs | Unset = UNSET, +) -> ChoreLoadResult | ErrorResponse | None: + """Load a published chore by publish external id. + + Args: + body (ChoreLoadPublicArgs | Unset): ChoreLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ChoreLoadResult | ErrorResponse + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/habits/habit_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/habits/habit_load_public.py new file mode 100644 index 000000000..52c57ec42 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/habits/habit_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.habit_load_public_args import HabitLoadPublicArgs +from ...models.habit_load_result import HabitLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: HabitLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/habit-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | HabitLoadResult | None: + if response.status_code == 200: + response_200 = HabitLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | HabitLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: HabitLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | HabitLoadResult]: + """Load a published habit by publish external id. + + Args: + body (HabitLoadPublicArgs | Unset): HabitLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | HabitLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: HabitLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | HabitLoadResult | None: + """Load a published habit by publish external id. + + Args: + body (HabitLoadPublicArgs | Unset): HabitLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | HabitLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: HabitLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | HabitLoadResult]: + """Load a published habit by publish external id. + + Args: + body (HabitLoadPublicArgs | Unset): HabitLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | HabitLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: HabitLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | HabitLoadResult | None: + """Load a published habit by publish external id. + + Args: + body (HabitLoadPublicArgs | Unset): HabitLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | HabitLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index dc1060c9f..ad635d64f 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -133,6 +133,7 @@ from .chore_find_result import ChoreFindResult from .chore_find_result_entry import ChoreFindResultEntry from .chore_load_args import ChoreLoadArgs +from .chore_load_public_args import ChoreLoadPublicArgs from .chore_load_result import ChoreLoadResult from .chore_regen_args import ChoreRegenArgs from .chore_remove_args import ChoreRemoveArgs @@ -308,6 +309,7 @@ from .habit_find_result import HabitFindResult from .habit_find_result_entry import HabitFindResultEntry from .habit_load_args import HabitLoadArgs +from .habit_load_public_args import HabitLoadPublicArgs from .habit_load_result import HabitLoadResult from .habit_regen_args import HabitRegenArgs from .habit_remove_args import HabitRemoveArgs @@ -1153,6 +1155,7 @@ "ChoreFindResult", "ChoreFindResultEntry", "ChoreLoadArgs", + "ChoreLoadPublicArgs", "ChoreLoadResult", "ChoreRegenArgs", "ChoreRemoveArgs", @@ -1328,6 +1331,7 @@ "HabitFindResult", "HabitFindResultEntry", "HabitLoadArgs", + "HabitLoadPublicArgs", "HabitLoadResult", "HabitRegenArgs", "HabitRemoveArgs", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/chore_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/chore_load_public_args.py new file mode 100644 index 000000000..4e4753b7d --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/chore_load_public_args.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="ChoreLoadPublicArgs") + + +@_attrs_define +class ChoreLoadPublicArgs: + """ChoreLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + inbox_task_retrieve_offset (int | None | Unset): + """ + + external_id: str + inbox_task_retrieve_offset: int | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + inbox_task_retrieve_offset: int | None | Unset + if isinstance(self.inbox_task_retrieve_offset, Unset): + inbox_task_retrieve_offset = UNSET + else: + inbox_task_retrieve_offset = self.inbox_task_retrieve_offset + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + if inbox_task_retrieve_offset is not UNSET: + field_dict["inbox_task_retrieve_offset"] = inbox_task_retrieve_offset + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + def _parse_inbox_task_retrieve_offset(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + inbox_task_retrieve_offset = _parse_inbox_task_retrieve_offset(d.pop("inbox_task_retrieve_offset", UNSET)) + + chore_load_public_args = cls( + external_id=external_id, + inbox_task_retrieve_offset=inbox_task_retrieve_offset, + ) + + chore_load_public_args.additional_properties = d + return chore_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/chore_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/chore_load_result.py index 03a88e400..4427c821e 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/chore_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/chore_load_result.py @@ -16,6 +16,7 @@ from ..models.goal import Goal from ..models.inbox_task import InboxTask from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_in_day_block import TimeEventInDayBlock @@ -39,6 +40,7 @@ class ChoreLoadResult: chapter (Chapter | None | Unset): goal (Goal | None | Unset): note (None | Note | Unset): + publish_entity (None | PublishEntity | Unset): """ chore: Chore @@ -52,12 +54,14 @@ class ChoreLoadResult: chapter: Chapter | None | Unset = UNSET goal: Goal | None | Unset = UNSET note: None | Note | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.chapter import Chapter from ..models.goal import Goal from ..models.note import Note + from ..models.publish_entity import PublishEntity chore = self.chore.to_dict() @@ -111,6 +115,14 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -131,6 +143,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["goal"] = goal if note is not UNSET: field_dict["note"] = note + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -143,6 +157,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.goal import Goal from ..models.inbox_task import InboxTask from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_in_day_block import TimeEventInDayBlock @@ -234,6 +249,23 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + chore_load_result = cls( chore=chore, aspect=aspect, @@ -246,6 +278,7 @@ def _parse_note(data: object) -> None | Note | Unset: chapter=chapter, goal=goal, note=note, + publish_entity=publish_entity, ) chore_load_result.additional_properties = d diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/habit_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/habit_load_public_args.py new file mode 100644 index 000000000..ab3242d20 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/habit_load_public_args.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="HabitLoadPublicArgs") + + +@_attrs_define +class HabitLoadPublicArgs: + """HabitLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + inbox_task_retrieve_offset (int | None | Unset): + include_streak_marks_earliest_date (None | str | Unset): + include_streak_marks_latest_date (None | str | Unset): + """ + + external_id: str + inbox_task_retrieve_offset: int | None | Unset = UNSET + include_streak_marks_earliest_date: None | str | Unset = UNSET + include_streak_marks_latest_date: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + inbox_task_retrieve_offset: int | None | Unset + if isinstance(self.inbox_task_retrieve_offset, Unset): + inbox_task_retrieve_offset = UNSET + else: + inbox_task_retrieve_offset = self.inbox_task_retrieve_offset + + include_streak_marks_earliest_date: None | str | Unset + if isinstance(self.include_streak_marks_earliest_date, Unset): + include_streak_marks_earliest_date = UNSET + else: + include_streak_marks_earliest_date = self.include_streak_marks_earliest_date + + include_streak_marks_latest_date: None | str | Unset + if isinstance(self.include_streak_marks_latest_date, Unset): + include_streak_marks_latest_date = UNSET + else: + include_streak_marks_latest_date = self.include_streak_marks_latest_date + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + if inbox_task_retrieve_offset is not UNSET: + field_dict["inbox_task_retrieve_offset"] = inbox_task_retrieve_offset + if include_streak_marks_earliest_date is not UNSET: + field_dict["include_streak_marks_earliest_date"] = include_streak_marks_earliest_date + if include_streak_marks_latest_date is not UNSET: + field_dict["include_streak_marks_latest_date"] = include_streak_marks_latest_date + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + def _parse_inbox_task_retrieve_offset(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + inbox_task_retrieve_offset = _parse_inbox_task_retrieve_offset(d.pop("inbox_task_retrieve_offset", UNSET)) + + def _parse_include_streak_marks_earliest_date(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + include_streak_marks_earliest_date = _parse_include_streak_marks_earliest_date( + d.pop("include_streak_marks_earliest_date", UNSET) + ) + + def _parse_include_streak_marks_latest_date(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + include_streak_marks_latest_date = _parse_include_streak_marks_latest_date( + d.pop("include_streak_marks_latest_date", UNSET) + ) + + habit_load_public_args = cls( + external_id=external_id, + inbox_task_retrieve_offset=inbox_task_retrieve_offset, + include_streak_marks_earliest_date=include_streak_marks_earliest_date, + include_streak_marks_latest_date=include_streak_marks_latest_date, + ) + + habit_load_public_args.additional_properties = d + return habit_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/habit_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/habit_load_result.py index 229f4f746..5e2f6fd38 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/habit_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/habit_load_result.py @@ -17,6 +17,7 @@ from ..models.habit_streak_mark import HabitStreakMark from ..models.inbox_task import InboxTask from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_in_day_block import TimeEventInDayBlock @@ -43,6 +44,7 @@ class HabitLoadResult: chapter (Chapter | None | Unset): goal (Goal | None | Unset): note (None | Note | Unset): + publish_entity (None | PublishEntity | Unset): """ habit: Habit @@ -59,12 +61,14 @@ class HabitLoadResult: chapter: Chapter | None | Unset = UNSET goal: Goal | None | Unset = UNSET note: None | Note | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.chapter import Chapter from ..models.goal import Goal from ..models.note import Note + from ..models.publish_entity import PublishEntity habit = self.habit.to_dict() @@ -127,6 +131,14 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -150,6 +162,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["goal"] = goal if note is not UNSET: field_dict["note"] = note + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -163,6 +177,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.habit_streak_mark import HabitStreakMark from ..models.inbox_task import InboxTask from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_in_day_block import TimeEventInDayBlock @@ -265,6 +280,23 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + habit_load_result = cls( habit=habit, aspect=aspect, @@ -280,6 +312,7 @@ def _parse_note(data: object) -> None | Note | Unset: chapter=chapter, goal=goal, note=note, + publish_entity=publish_entity, ) habit_load_result.additional_properties = d diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index f229a19c9..dd7278d66 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -116,6 +116,7 @@ export type { ChoreFindArgs } from './models/ChoreFindArgs'; export type { ChoreFindResult } from './models/ChoreFindResult'; export type { ChoreFindResultEntry } from './models/ChoreFindResultEntry'; export type { ChoreLoadArgs } from './models/ChoreLoadArgs'; +export type { ChoreLoadPublicArgs } from './models/ChoreLoadPublicArgs'; export type { ChoreLoadResult } from './models/ChoreLoadResult'; export type { ChoreName } from './models/ChoreName'; export type { ChoreRegenArgs } from './models/ChoreRegenArgs'; @@ -269,6 +270,7 @@ export type { HabitFindArgs } from './models/HabitFindArgs'; export type { HabitFindResult } from './models/HabitFindResult'; export type { HabitFindResultEntry } from './models/HabitFindResultEntry'; export type { HabitLoadArgs } from './models/HabitLoadArgs'; +export type { HabitLoadPublicArgs } from './models/HabitLoadPublicArgs'; export type { HabitLoadResult } from './models/HabitLoadResult'; export type { HabitName } from './models/HabitName'; export type { HabitRegenArgs } from './models/HabitRegenArgs'; diff --git a/gen/ts/webapi-client/gen/models/ChoreLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/ChoreLoadPublicArgs.ts new file mode 100644 index 000000000..72ddef58d --- /dev/null +++ b/gen/ts/webapi-client/gen/models/ChoreLoadPublicArgs.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * ChoreLoadPublic args. + */ +export type ChoreLoadPublicArgs = { + external_id: PublishExternalId; + inbox_task_retrieve_offset?: (number | null); +}; + diff --git a/gen/ts/webapi-client/gen/models/ChoreLoadResult.ts b/gen/ts/webapi-client/gen/models/ChoreLoadResult.ts index f55d641c9..e88db8296 100644 --- a/gen/ts/webapi-client/gen/models/ChoreLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/ChoreLoadResult.ts @@ -9,6 +9,7 @@ import type { Contact } from './Contact'; import type { Goal } from './Goal'; import type { InboxTask } from './InboxTask'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { Tag } from './Tag'; import type { TimeEventInDayBlock } from './TimeEventInDayBlock'; /** @@ -26,5 +27,6 @@ export type ChoreLoadResult = { contacts: Array<Contact>; note?: (Note | null); time_event_blocks: Array<TimeEventInDayBlock>; + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/models/HabitLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/HabitLoadPublicArgs.ts new file mode 100644 index 000000000..e3921840a --- /dev/null +++ b/gen/ts/webapi-client/gen/models/HabitLoadPublicArgs.ts @@ -0,0 +1,16 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ADate } from './ADate'; +import type { PublishExternalId } from './PublishExternalId'; +/** + * HabitLoadPublic args. + */ +export type HabitLoadPublicArgs = { + external_id: PublishExternalId; + inbox_task_retrieve_offset?: (number | null); + include_streak_marks_earliest_date?: (ADate | null); + include_streak_marks_latest_date?: (ADate | null); +}; + diff --git a/gen/ts/webapi-client/gen/models/HabitLoadResult.ts b/gen/ts/webapi-client/gen/models/HabitLoadResult.ts index c12696fa5..6fc06602e 100644 --- a/gen/ts/webapi-client/gen/models/HabitLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/HabitLoadResult.ts @@ -11,6 +11,7 @@ import type { Habit } from './Habit'; import type { HabitStreakMark } from './HabitStreakMark'; import type { InboxTask } from './InboxTask'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { Tag } from './Tag'; import type { TimeEventInDayBlock } from './TimeEventInDayBlock'; /** @@ -31,5 +32,6 @@ export type HabitLoadResult = { contacts: Array<Contact>; note?: (Note | null); time_event_blocks: Array<TimeEventInDayBlock>; + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/services/ChoresService.ts b/gen/ts/webapi-client/gen/services/ChoresService.ts index af0c3495c..b1b70f2db 100644 --- a/gen/ts/webapi-client/gen/services/ChoresService.ts +++ b/gen/ts/webapi-client/gen/services/ChoresService.ts @@ -8,6 +8,7 @@ import type { ChoreCreateResult } from '../models/ChoreCreateResult'; import type { ChoreFindArgs } from '../models/ChoreFindArgs'; import type { ChoreFindResult } from '../models/ChoreFindResult'; import type { ChoreLoadArgs } from '../models/ChoreLoadArgs'; +import type { ChoreLoadPublicArgs } from '../models/ChoreLoadPublicArgs'; import type { ChoreLoadResult } from '../models/ChoreLoadResult'; import type { ChoreRegenArgs } from '../models/ChoreRegenArgs'; import type { ChoreRemoveArgs } from '../models/ChoreRemoveArgs'; @@ -130,6 +131,34 @@ export class ChoresService { }, }); } + /** + * Load a published chore by publish external id. + * @param requestBody The input data + * @returns ChoreLoadResult Successful response + * @throws ApiError + */ + public choreLoadPublic( + requestBody?: ChoreLoadPublicArgs, + ): CancelablePromise<ChoreLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/chore-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * A use case for regenerating tasks associated with chores. * @param requestBody The input data diff --git a/gen/ts/webapi-client/gen/services/HabitsService.ts b/gen/ts/webapi-client/gen/services/HabitsService.ts index 311147f3d..b79b18987 100644 --- a/gen/ts/webapi-client/gen/services/HabitsService.ts +++ b/gen/ts/webapi-client/gen/services/HabitsService.ts @@ -8,6 +8,7 @@ import type { HabitCreateResult } from '../models/HabitCreateResult'; import type { HabitFindArgs } from '../models/HabitFindArgs'; import type { HabitFindResult } from '../models/HabitFindResult'; import type { HabitLoadArgs } from '../models/HabitLoadArgs'; +import type { HabitLoadPublicArgs } from '../models/HabitLoadPublicArgs'; import type { HabitLoadResult } from '../models/HabitLoadResult'; import type { HabitRegenArgs } from '../models/HabitRegenArgs'; import type { HabitRemoveArgs } from '../models/HabitRemoveArgs'; @@ -130,6 +131,34 @@ export class HabitsService { }, }); } + /** + * Load a published habit by publish external id. + * @param requestBody The input data + * @returns HabitLoadResult Successful response + * @throws ApiError + */ + public habitLoadPublic( + requestBody?: HabitLoadPublicArgs, + ): CancelablePromise<HabitLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/habit-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * A use case for regenerating tasks associated with habits. * @param requestBody The input data diff --git a/itests/webui/entities/chores.test.py b/itests/webui/entities/chores.test.py index 78f22aaf1..1fb2b50bd 100644 --- a/itests/webui/entities/chores.test.py +++ b/itests/webui/entities/chores.test.py @@ -1,5 +1,7 @@ """Tests about chores.""" +import re + import pytest from jupiter_webapi_client.api.chores.chore_create import ( sync_detailed as chore_create_sync, @@ -20,7 +22,7 @@ ) from playwright.sync_api import Page, expect -from itests.helpers import get_parsed_from_response +from itests.helpers import get_parsed_from_response, open_leaf_publish_panel @pytest.fixture(autouse=True, scope="module") @@ -101,3 +103,33 @@ def test_webui_chore_view_all(page: Page, create_chore) -> None: expect(page.locator(f"#chore-{chore1.ref_id}")).to_contain_text("Chore 1") expect(page.locator(f"#chore-{chore2.ref_id}")).to_contain_text("Chore 2") expect(page.locator(f"#chore-{chore3.ref_id}")).to_contain_text("Chore 3") + + +def test_webui_chore_publish_and_view_public(page: Page, create_chore) -> None: + chore = create_chore("Published Chore") + page.goto(f"/app/workspace/chores/{chore.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Chore-publish") + page.locator("button[id='Chore-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/chores/{chore.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Chore-publish") + expect(page.locator("#Chore-publish")).to_contain_text("draft") + + page.locator("button[id='Chore-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/chores/{chore.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Chore-publish") + expect(page.locator("#Chore-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/chore/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="name"]')).to_have_value("Published Chore") diff --git a/itests/webui/entities/habits.test.py b/itests/webui/entities/habits.test.py index d9fffc8cc..c243f23e6 100644 --- a/itests/webui/entities/habits.test.py +++ b/itests/webui/entities/habits.test.py @@ -1,5 +1,7 @@ """Tests about habits.""" +import re + import pytest from jupiter_webapi_client.api.habits.habit_create import ( sync_detailed as habit_create_sync, @@ -21,7 +23,7 @@ ) from playwright.sync_api import Page, expect -from itests.helpers import get_parsed_from_response +from itests.helpers import get_parsed_from_response, open_leaf_publish_panel @pytest.fixture(autouse=True, scope="module") @@ -101,3 +103,33 @@ def test_webui_habit_view_all(page: Page, create_habit) -> None: expect(page.locator(f"#habit-{habit1.ref_id}")).to_contain_text("Habit 1") expect(page.locator(f"#habit-{habit2.ref_id}")).to_contain_text("Habit 2") expect(page.locator(f"#habit-{habit3.ref_id}")).to_contain_text("Habit 3") + + +def test_webui_habit_publish_and_view_public(page: Page, create_habit) -> None: + habit = create_habit("Published Habit") + page.goto(f"/app/workspace/habits/{habit.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Habit-publish") + page.locator("button[id='Habit-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/habits/{habit.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Habit-publish") + expect(page.locator("#Habit-publish")).to_contain_text("draft") + + page.locator("button[id='Habit-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/habits/{habit.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "Habit-publish") + expect(page.locator("#Habit-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/habit/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="name"]')).to_have_value("Published Habit") diff --git a/src/core/jupiter/core/chores/component/editor.tsx b/src/core/jupiter/core/chores/component/editor.tsx new file mode 100644 index 000000000..34d8181ff --- /dev/null +++ b/src/core/jupiter/core/chores/component/editor.tsx @@ -0,0 +1,165 @@ +import type { + Aspect, + Chapter, + Contact, + Chore, + Goal, + Tag, +} from "@jupiter/webapi-client"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import { + FormControl, + FormControlLabel, + InputLabel, + OutlinedInput, + Stack, + Switch, +} from "@mui/material"; + +import { aDateToDate } from "#/core/common/adate"; +import { entityLinkStd } from "#/core/common/entity-link"; +import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; +import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; +import { RecurringTaskGenParamsBlock } from "#/core/common/component/recurring-task-gen-params-block"; +import { IsKeySelect } from "#/core/common/component/is-key-select"; +import { SlimChip } from "#/core/infra/component/chips"; +import { SectionCard } from "#/core/infra/component/section-card"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import type { TopLevelInfo } from "#/core/infra/top-level-context"; + +interface ChoreEditorProps { + chore: Chore; + tags: Array<Tag>; + contacts: Array<Contact>; + allTags: Array<Tag>; + allContacts: Array<Contact>; + aspect: Aspect; + chapter: Chapter | null; + goal: Goal | null; + inputsEnabled: boolean; + topLevelInfo: TopLevelInfo; +} + +export function ChoreEditor(props: ChoreEditorProps) { + const isBigScreen = useBigScreen(); + const { chore, tags, contacts, allTags, allContacts, aspect, chapter, goal } = + props; + + return ( + <SectionCard title="Properties"> + <Stack direction="row" useFlexGap spacing={1}> + <FormControl fullWidth sx={{ flexGrow: 3 }}> + <InputLabel id="name">Name</InputLabel> + <OutlinedInput + label="Name" + name="name" + readOnly={!props.inputsEnabled} + defaultValue={chore.name} + /> + </FormControl> + + <FormControl sx={{ flexGrow: 1 }}> + <IsKeySelect + name="isKey" + defaultValue={chore.is_key} + inputsEnabled={props.inputsEnabled} + /> + </FormControl> + </Stack> + + <Stack direction={isBigScreen ? "row" : "column"} useFlexGap spacing={1}> + <FormControl sx={{ flexGrow: 2 }}> + <TagsEditor + name="tags" + aloneOnLine + allTags={allTags} + defaultValue={tags.map((tag) => tag.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.CHORE, chore.ref_id)} + /> + </FormControl> + + <FormControl sx={{ flexGrow: 2 }}> + <ContactsEditor + name="contacts_names" + aloneOnLine + allContacts={allContacts} + defaultValue={contacts.map((contact) => contact.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.CHORE, chore.ref_id)} + /> + </FormControl> + </Stack> + + <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap> + <SlimChip label={`Aspect: ${aspect.name}`} color="default" /> + {chapter !== null && ( + <SlimChip label={`Chapter: ${chapter.name}`} color="default" /> + )} + {goal !== null && ( + <SlimChip label={`Goal: ${goal.name}`} color="default" /> + )} + </Stack> + + <RecurringTaskGenParamsBlock + inputsEnabled={props.inputsEnabled} + allowSkipRule + period={chore.gen_params.period} + eisen={chore.gen_params.eisen} + difficulty={chore.gen_params.difficulty} + actionableFromDay={chore.gen_params.actionable_from_day} + actionableFromMonth={chore.gen_params.actionable_from_month} + dueAtDay={chore.gen_params.due_at_day} + dueAtMonth={chore.gen_params.due_at_month} + skipRule={chore.gen_params.skip_rule} + /> + + <FormControl fullWidth> + <FormControlLabel + control={ + <Switch + name="mustDo" + readOnly={!props.inputsEnabled} + defaultChecked={chore.must_do} + /> + } + label="Must Do In Vacation" + /> + </FormControl> + + <Stack spacing={2} direction={isBigScreen ? "row" : "column"}> + <FormControl fullWidth> + <InputLabel id="startAtDate" shrink> + Start At Date [Optional] + </InputLabel> + <OutlinedInput + label="Start At Date [Optional]" + name="startAtDate" + readOnly={!props.inputsEnabled} + defaultValue={ + chore.start_at_date + ? aDateToDate(chore.start_at_date).toISODate() + : "" + } + /> + </FormControl> + + <FormControl fullWidth> + <InputLabel id="endAtDate" shrink> + End At Date [Optional] + </InputLabel> + <OutlinedInput + label="End At Date [Optional]" + name="endAtDate" + readOnly={!props.inputsEnabled} + defaultValue={ + chore.end_at_date + ? aDateToDate(chore.end_at_date).toISODate() + : "" + } + /> + </FormControl> + </Stack> + </SectionCard> + ); +} diff --git a/src/core/jupiter/core/chores/root.py b/src/core/jupiter/core/chores/root.py index 31e3c72dd..c5a872171 100644 --- a/src/core/jupiter/core/chores/root.py +++ b/src/core/jupiter/core/chores/root.py @@ -4,6 +4,7 @@ from jupiter.core.common.recurring_task_gen_params import RecurringTaskGenParams from jupiter.core.common.sub.inbox_tasks.root import InboxTask from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( TimeEventInDayBlock, @@ -52,6 +53,9 @@ class Chore(LeafEntity): ) tag_link = OwnsAtMostOne(TagLink, owner=IsEntityLinkStd(NamedEntityTag.CHORE.value)) note = OwnsAtMostOne(Note, owner=IsEntityLinkStd(NamedEntityTag.CHORE.value)) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.CHORE.value) + ) @staticmethod @create_entity_action diff --git a/src/core/jupiter/core/chores/service/__init__.py b/src/core/jupiter/core/chores/service/__init__.py index da810ec92..bf6fe853c 100644 --- a/src/core/jupiter/core/chores/service/__init__.py +++ b/src/core/jupiter/core/chores/service/__init__.py @@ -1 +1 @@ -"""Chore service classes.""" +"""Chore services.""" diff --git a/src/core/jupiter/core/chores/service/load.py b/src/core/jupiter/core/chores/service/load.py new file mode 100644 index 000000000..3c830d01c --- /dev/null +++ b/src/core/jupiter/core/chores/service/load.py @@ -0,0 +1,160 @@ +"""Shared service for loading a chore and its dependent entities.""" + +from jupiter.core.chores.root import Chore +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.inbox_tasks.collection import InboxTaskCollection +from jupiter.core.common.sub.inbox_tasks.root import ( + InboxTask, + InboxTaskRepository, +) +from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.common.sub.time_events.domain import TimeEventDomain +from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( + TimeEventInDayBlock, +) +from jupiter.core.life_plan.sub.aspects.root import Aspect +from jupiter.core.life_plan.sub.chapters.root import Chapter +from jupiter.core.life_plan.sub.goals.root import Goal +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result + + +@use_case_result +class ChoreLoadResult(UseCaseResultBase): + """ChoreLoadResult.""" + + chore: Chore + aspect: Aspect + chapter: Chapter | None + goal: Goal | None + inbox_tasks: list[InboxTask] + inbox_tasks_total_cnt: int + inbox_tasks_page_size: int + tags: list[Tag] + contacts: list[Contact] + note: Note | None + time_event_blocks: list[TimeEventInDayBlock] + publish_entity: PublishEntity | None + + +class ChoreLoadService: + """Shared service for loading a chore and its dependent entities.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + chore: Chore, + *, + allow_archived: bool = False, + inbox_task_retrieve_offset: int = 0, + include_publish_entity: bool = True, + ) -> ChoreLoadResult: + """Load a chore and its dependent entities.""" + chore = await uow.get_for(Chore).load_by_id( + chore.ref_id, allow_archived=allow_archived + ) + aspect = await uow.get_for(Aspect).load_by_id(chore.aspect_ref_id) + chapter = ( + await uow.get_for(Chapter).load_by_id(chore.chapter_ref_id) + if chore.chapter_ref_id + else None + ) + goal = ( + await uow.get_for(Goal).load_by_id(chore.goal_ref_id) + if chore.goal_ref_id + else None + ) + inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( + workspace_ref_id, + ) + + inbox_tasks_total_cnt = await uow.get(InboxTaskRepository).count_all_for_owner( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=allow_archived, + owner=EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), + ) + inbox_tasks = await uow.get( + InboxTaskRepository + ).find_all_for_owner_created_desc( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=True, + owner=EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), + retrieve_offset=inbox_task_retrieve_offset, + retrieve_limit=InboxTaskRepository.PAGE_SIZE, + ) + + note = await uow.get(NoteRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), + allow_archived=allow_archived, + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace_ref_id, + ) + contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), + ) + if contact_link is not None: + contacts = await uow.get_for(Contact).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + ref_id=contact_link.contacts_ref_ids, + ) + else: + contacts = [] + + time_event_domain = await uow.get_for(TimeEventDomain).load_by_parent( + workspace_ref_id + ) + time_event_blocks = await uow.get_for(TimeEventInDayBlock).find_all_generic( + parent_ref_id=time_event_domain.ref_id, + allow_archived=False, + owner=EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), + ) + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), + allow_archived=allow_archived, + ) + + return ChoreLoadResult( + chore=chore, + aspect=aspect, + chapter=chapter, + goal=goal, + inbox_tasks=inbox_tasks, + inbox_tasks_total_cnt=inbox_tasks_total_cnt, + inbox_tasks_page_size=InboxTaskRepository.PAGE_SIZE, + tags=tags, + contacts=contacts, + note=note, + time_event_blocks=time_event_blocks, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/chores/use_case/load.py b/src/core/jupiter/core/chores/use_case/load.py index 286aa2f63..658f275e9 100644 --- a/src/core/jupiter/core/chores/use_case/load.py +++ b/src/core/jupiter/core/chores/use_case/load.py @@ -1,34 +1,13 @@ """Use case for loading a particular chore.""" from jupiter.core.chores.root import Chore -from jupiter.core.common.sub.contacts.root import ContactDomain -from jupiter.core.common.sub.contacts.sub.contact.root import Contact -from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository -from jupiter.core.common.sub.inbox_tasks.collection import ( - InboxTaskCollection, -) -from jupiter.core.common.sub.inbox_tasks.root import ( - InboxTask, - InboxTaskRepository, -) -from jupiter.core.common.sub.notes.root import Note, NoteRepository -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository -from jupiter.core.common.sub.time_events.domain import TimeEventDomain -from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( - TimeEventInDayBlock, -) +from jupiter.core.chores.service.load import ChoreLoadResult, ChoreLoadService from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.life_plan.sub.aspects.root import Aspect -from jupiter.core.life_plan.sub.chapters.root import Chapter -from jupiter.core.life_plan.sub.goals.root import Goal -from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.errors import InputValidationError from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import ( @@ -36,11 +15,11 @@ ) from jupiter.framework.use_case_io import ( UseCaseArgsBase, - UseCaseResultBase, use_case_args, - use_case_result, ) +__all__ = ["ChoreLoadArgs", "ChoreLoadResult", "ChoreLoadUseCase"] + @use_case_args class ChoreLoadArgs(UseCaseArgsBase): @@ -51,23 +30,6 @@ class ChoreLoadArgs(UseCaseArgsBase): inbox_task_retrieve_offset: int | None -@use_case_result -class ChoreLoadResult(UseCaseResultBase): - """ChoreLoadResult.""" - - chore: Chore - aspect: Aspect - chapter: Chapter | None - goal: Goal | None - inbox_tasks: list[InboxTask] - inbox_tasks_total_cnt: int - inbox_tasks_page_size: int - tags: list[Tag] - contacts: list[Contact] - note: Note | None - time_event_blocks: list[TimeEventInDayBlock] - - @readonly_use_case(WorkspaceFeature.CHORES) class ChoreLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[ChoreLoadArgs, ChoreLoadResult] @@ -92,86 +54,11 @@ async def _perform_transactional_read( chore = await uow.get_for(Chore).load_by_id( args.ref_id, allow_archived=allow_archived ) - aspect = await uow.get_for(Aspect).load_by_id(chore.aspect_ref_id) - chapter = ( - await uow.get_for(Chapter).load_by_id(chore.chapter_ref_id) - if chore.chapter_ref_id - else None - ) - goal = ( - await uow.get_for(Goal).load_by_id(chore.goal_ref_id) - if chore.goal_ref_id - else None - ) - inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( - workspace.ref_id, - ) - - inbox_tasks_total_cnt = await uow.get(InboxTaskRepository).count_all_for_owner( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=allow_archived, - owner=EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), - ) - inbox_tasks = await uow.get( - InboxTaskRepository - ).find_all_for_owner_created_desc( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=True, - owner=EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), - retrieve_offset=args.inbox_task_retrieve_offset or 0, - retrieve_limit=InboxTaskRepository.PAGE_SIZE, - ) - note = await uow.get(NoteRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), - allow_archived=allow_archived, - ) - - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - contact_domain = await uow.get_for(ContactDomain).load_by_parent( + return await ChoreLoadService().do_it( + uow, workspace.ref_id, - ) - contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), - ) - if contact_link is not None: - contacts = await uow.get_for(Contact).find_all_generic( - parent_ref_id=contact_domain.ref_id, - allow_archived=False, - ref_id=contact_link.contacts_ref_ids, - ) - else: - contacts = [] - - time_event_domain = await uow.get_for(TimeEventDomain).load_by_parent( - workspace.ref_id - ) - time_event_blocks = await uow.get_for(TimeEventInDayBlock).find_all_generic( - parent_ref_id=time_event_domain.ref_id, - allow_archived=False, - owner=EntityLink.std(NamedEntityTag.CHORE.value, chore.ref_id), - ) - - return ChoreLoadResult( - chore=chore, - aspect=aspect, - chapter=chapter, - goal=goal, - inbox_tasks=inbox_tasks, - inbox_tasks_total_cnt=inbox_tasks_total_cnt, - inbox_tasks_page_size=InboxTaskRepository.PAGE_SIZE, - tags=tags, - contacts=contacts, - note=note, - time_event_blocks=time_event_blocks, + chore, + allow_archived=allow_archived, + inbox_task_retrieve_offset=args.inbox_task_retrieve_offset or 0, ) diff --git a/src/core/jupiter/core/chores/use_case/load_public.py b/src/core/jupiter/core/chores/use_case/load_public.py new file mode 100644 index 000000000..1075dafdf --- /dev/null +++ b/src/core/jupiter/core/chores/use_case/load_public.py @@ -0,0 +1,85 @@ +"""Guest readonly use case for loading a published chore.""" + +from jupiter.core.chores.collection import ChoreCollection +from jupiter.core.chores.root import Chore +from jupiter.core.chores.service.load import ChoreLoadResult, ChoreLoadService +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class ChoreLoadPublicArgs(UseCaseArgsBase): + """ChoreLoadPublic args.""" + + external_id: PublishExternalId + inbox_task_retrieve_offset: int | None + + +class ChoreLoadPublicUseCase( + JupiterGuestReadonlyUseCase[ChoreLoadPublicArgs, ChoreLoadResult] +): + """Load a published chore by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: ChoreLoadPublicArgs, + ) -> ChoreLoadResult: + """Execute the use case.""" + if ( + args.inbox_task_retrieve_offset is not None + and args.inbox_task_retrieve_offset < 0 + ): + raise InputValidationError("Invalid inbox_task_retrieve_offset") + + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.CHORE.value: + raise InputValidationError( + "The publish entity does not refer to a chore." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + chore_collection = await uow.get_for(ChoreCollection).load_by_parent( + publish_domain.workspace.ref_id + ) + chore = await uow.get_for(Chore).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if chore.parent_ref_id != chore_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace chore." + ) + + return await ChoreLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + chore, + allow_archived=False, + inbox_task_retrieve_offset=args.inbox_task_retrieve_offset or 0, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/common/sub/inbox_tasks/component/card.tsx b/src/core/jupiter/core/common/sub/inbox_tasks/component/card.tsx index ec2478baf..f1279b647 100644 --- a/src/core/jupiter/core/common/sub/inbox_tasks/component/card.tsx +++ b/src/core/jupiter/core/common/sub/inbox_tasks/component/card.tsx @@ -52,7 +52,7 @@ import { HabitTag } from "#/core/habits/component/habit-tag"; import { InboxTaskNamespaceTag } from "#/core/common/sub/inbox_tasks/component/namespace-tag"; import { parentLinkNamespaceFromEntityLinkWire } from "#/core/common/sub/inbox_tasks/parent-link-namespace"; import { InboxTaskStatusTag } from "#/core/common/sub/inbox_tasks/component/status-tag"; -import { EntityLink } from "#/core/infra/component/entity-card"; +import { EntityFakeLink, EntityLink } from "#/core/infra/component/entity-card"; import { MetricTag } from "#/core/metrics/component/tag"; import { ContactTag as ParentContactTag } from "#/core/common/sub/contacts/sub/contact/component/tag"; import { SlackTaskTag } from "#/core/push_integrations/sub/slack/component/tag"; @@ -82,6 +82,7 @@ export interface InboxTaskCardProps { optimisticState?: InboxTaskOptimisticState; parent?: InboxTaskParent; linkResolver?: (it: InboxTask, parent?: InboxTaskParent) => string; + linksEnabled?: boolean; onClick?: (it: InboxTask) => void; onMarkDone?: (it: InboxTask) => void; onMarkNotDone?: (it: InboxTask) => void; @@ -150,10 +151,10 @@ export function InboxTaskCard(props: InboxTaskCardProps) { const inputsEnabled = props.inboxTask.archived === false && !handlerInProgress; + const linksEnabled = props.linksEnabled ?? true; const targetLink = props.linkResolver ? props.linkResolver(props.inboxTask, props.parent) : `/app/workspace/core/inbox-tasks/${props.inboxTask.ref_id}`; - return ( <motion.div drag={inputsEnabled && props.allowSwipe ? "x" : false} @@ -185,17 +186,27 @@ export function InboxTaskCard(props: InboxTaskCardProps) { paddingBottom: "0.5rem", }} > - <EntityLink - to={targetLink} - block={props.onClick !== undefined} - inline - > - <IsKeyTag isKey={props.inboxTask.is_key} /> - <EntityNameComponent - compact={props.compact} - name={props.inboxTask.name} - /> - </EntityLink> + {linksEnabled ? ( + <EntityLink + to={targetLink} + block={props.onClick !== undefined} + inline + > + <IsKeyTag isKey={props.inboxTask.is_key} /> + <EntityNameComponent + compact={props.compact} + name={props.inboxTask.name} + /> + </EntityLink> + ) : ( + <EntityFakeLink inline> + <IsKeyTag isKey={props.inboxTask.is_key} /> + <EntityNameComponent + compact={props.compact} + name={props.inboxTask.name} + /> + </EntityFakeLink> + )} <TagsContained> {props.showOptions.showStatus && ( <InboxTaskStatusTag diff --git a/src/core/jupiter/core/common/sub/inbox_tasks/component/stack.tsx b/src/core/jupiter/core/common/sub/inbox_tasks/component/stack.tsx index a196d8c76..3ec4f3a97 100644 --- a/src/core/jupiter/core/common/sub/inbox_tasks/component/stack.tsx +++ b/src/core/jupiter/core/common/sub/inbox_tasks/component/stack.tsx @@ -29,6 +29,7 @@ interface InboxTaskStackProps { }; withPages?: PagesProps; cardLinkResolver?: (it: InboxTask, parent?: InboxTaskParent) => string; + linksEnabled?: boolean; onCardMarkDone?: (it: InboxTask) => void; onCardMarkNotDone?: (it: InboxTask) => void; } @@ -84,6 +85,7 @@ export function InboxTaskStack(props: InboxTaskStackProps) { optimisticState={props.optimisticUpdates?.[it.ref_id]} parent={props.moreInfoByRefId?.[it.ref_id]} linkResolver={props.cardLinkResolver} + linksEnabled={props.linksEnabled} onMarkDone={handleMarkDone} onMarkNotDone={handleMarkNotDone} /> diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index 3fd91274e..bf1cafda9 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -31,8 +31,8 @@ NamedEntityTag.SCHEDULE_STREAM.value, NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, # done NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, # done - NamedEntityTag.HABIT.value, - NamedEntityTag.CHORE.value, + NamedEntityTag.HABIT.value, # done + NamedEntityTag.CHORE.value, # done NamedEntityTag.BIG_PLAN.value, NamedEntityTag.DOC.value, # done NamedEntityTag.DIR.value, diff --git a/src/core/jupiter/core/habits/component/editor.tsx b/src/core/jupiter/core/habits/component/editor.tsx new file mode 100644 index 000000000..21089813d --- /dev/null +++ b/src/core/jupiter/core/habits/component/editor.tsx @@ -0,0 +1,178 @@ +import type { + Aspect, + Chapter, + Contact, + Goal, + Habit, + HabitStreakMark, + Tag, +} from "@jupiter/webapi-client"; +import { + HabitRepeatsStrategy, + NamedEntityTag, + RecurringTaskPeriod, +} from "@jupiter/webapi-client"; +import { FormControl, InputLabel, OutlinedInput, Stack } from "@mui/material"; +import { useState } from "react"; + +import { entityLinkStd } from "#/core/common/entity-link"; +import { ContactsEditor } from "#/core/common/sub/contacts/component/contacts-editor"; +import { TagsEditor } from "#/core/common/sub/tags/component/tags-editor"; +import { RecurringTaskGenParamsBlock } from "#/core/common/component/recurring-task-gen-params-block"; +import { IsKeySelect } from "#/core/common/component/is-key-select"; +import { HabitRepeatStrategySelect } from "#/core/habits/component/repeat-strategy-select"; +import { HabitStreakCalendar } from "#/core/habits/component/streak-calendar"; +import { SlimChip } from "#/core/infra/component/chips"; +import { SectionCard } from "#/core/infra/component/section-card"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import type { TopLevelInfo } from "#/core/infra/top-level-context"; + +interface HabitEditorProps { + habit: Habit; + tags: Array<Tag>; + contacts: Array<Contact>; + allTags: Array<Tag>; + allContacts: Array<Contact>; + aspect: Aspect; + chapter: Chapter | null; + goal: Goal | null; + inputsEnabled: boolean; + topLevelInfo: TopLevelInfo; + streakMarks?: Array<HabitStreakMark>; + streakMarkEarliestDate?: string; + streakMarkLatestDate?: string; + showStreak?: boolean; +} + +export function HabitEditor(props: HabitEditorProps) { + const isBigScreen = useBigScreen(); + const { habit, tags, contacts, allTags, allContacts, aspect, chapter, goal } = + props; + + const [selectedRepeatsStrategy, setSelectedRepeatsStrategy] = useState< + HabitRepeatsStrategy | "none" + >(habit.repeats_strategy || "none"); + + return ( + <> + <SectionCard title="Properties"> + <Stack direction="row" useFlexGap spacing={1}> + <FormControl sx={{ flexGrow: 3 }}> + <InputLabel id="name">Name</InputLabel> + <OutlinedInput + label="Name" + name="name" + readOnly={!props.inputsEnabled} + defaultValue={habit.name} + /> + </FormControl> + + <FormControl sx={{ flexGrow: 1 }}> + <IsKeySelect + name="isKey" + defaultValue={habit.is_key} + inputsEnabled={props.inputsEnabled} + /> + </FormControl> + </Stack> + + <Stack + direction={isBigScreen ? "row" : "column"} + useFlexGap + spacing={1} + > + <FormControl fullWidth sx={{ flexGrow: 2 }}> + <TagsEditor + name="tags" + aloneOnLine + allTags={allTags} + defaultValue={tags.map((tag) => tag.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.HABIT, habit.ref_id)} + /> + </FormControl> + + <FormControl fullWidth sx={{ flexGrow: 2 }}> + <ContactsEditor + name="contacts_names" + aloneOnLine + allContacts={allContacts} + defaultValue={contacts.map((contact) => contact.ref_id)} + inputsEnabled={props.inputsEnabled} + owner={entityLinkStd(NamedEntityTag.HABIT, habit.ref_id)} + /> + </FormControl> + </Stack> + + <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap> + <SlimChip label={`Aspect: ${aspect.name}`} color="default" /> + {chapter !== null && ( + <SlimChip label={`Chapter: ${chapter.name}`} color="default" /> + )} + {goal !== null && ( + <SlimChip label={`Goal: ${goal.name}`} color="default" /> + )} + </Stack> + + <RecurringTaskGenParamsBlock + allowSkipRule + inputsEnabled={props.inputsEnabled} + period={habit.gen_params.period} + eisen={habit.gen_params.eisen} + difficulty={habit.gen_params.difficulty} + actionableFromDay={habit.gen_params.actionable_from_day} + actionableFromMonth={habit.gen_params.actionable_from_month} + dueAtDay={habit.gen_params.due_at_day} + dueAtMonth={habit.gen_params.due_at_month} + skipRule={habit.gen_params.skip_rule} + /> + + {habit.gen_params.period !== RecurringTaskPeriod.DAILY && ( + <Stack direction="row" spacing={2}> + <FormControl sx={{ flexGrow: 3 }}> + <HabitRepeatStrategySelect + name="repeatsStrategy" + inputsEnabled={props.inputsEnabled} + allowNone + value={selectedRepeatsStrategy} + onChange={(newStrategy) => + setSelectedRepeatsStrategy(newStrategy) + } + /> + </FormControl> + + {selectedRepeatsStrategy !== "none" && ( + <FormControl sx={{ flexGrow: 1 }}> + <InputLabel id="repeatsInPeriodCount"> + Repeats In Period [Optional] + </InputLabel> + <OutlinedInput + label="Repeats In Period" + name="repeatsInPeriodCount" + readOnly={!props.inputsEnabled} + defaultValue={habit.repeats_in_period_count} + sx={{ height: "100%" }} + /> + </FormControl> + )} + </Stack> + )} + </SectionCard> + + {props.showStreak && + props.streakMarks !== undefined && + props.streakMarkEarliestDate !== undefined && + props.streakMarkLatestDate !== undefined && ( + <SectionCard title="Streak"> + <HabitStreakCalendar + earliestDate={props.streakMarkEarliestDate} + latestDate={props.streakMarkLatestDate} + currentToday={props.topLevelInfo.today} + habit={habit} + streakMarks={props.streakMarks} + /> + </SectionCard> + )} + </> + ); +} diff --git a/src/core/jupiter/core/habits/root.py b/src/core/jupiter/core/habits/root.py index d706378a0..e3afe95be 100644 --- a/src/core/jupiter/core/habits/root.py +++ b/src/core/jupiter/core/habits/root.py @@ -4,6 +4,7 @@ from jupiter.core.common.recurring_task_period import RecurringTaskPeriod from jupiter.core.common.sub.inbox_tasks.root import InboxTask from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( TimeEventInDayBlock, @@ -57,6 +58,9 @@ class Habit(LeafEntity): ) tag_link = OwnsAtMostOne(TagLink, owner=IsEntityLinkStd(NamedEntityTag.HABIT.value)) note = OwnsAtMostOne(Note, owner=IsEntityLinkStd(NamedEntityTag.HABIT.value)) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.HABIT.value) + ) streak_marks = ContainsManyRecords( HabitStreakMark, habit_ref_id=IsRefId(), diff --git a/src/core/jupiter/core/habits/service/__init__.py b/src/core/jupiter/core/habits/service/__init__.py index af6290b34..f47f33e6e 100644 --- a/src/core/jupiter/core/habits/service/__init__.py +++ b/src/core/jupiter/core/habits/service/__init__.py @@ -1 +1 @@ -"""Habits services.""" +"""Habit services.""" diff --git a/src/core/jupiter/core/habits/service/load.py b/src/core/jupiter/core/habits/service/load.py new file mode 100644 index 000000000..65f0fa9c9 --- /dev/null +++ b/src/core/jupiter/core/habits/service/load.py @@ -0,0 +1,192 @@ +"""Shared service for loading a habit and its dependent entities.""" + +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.inbox_tasks.collection import InboxTaskCollection +from jupiter.core.common.sub.inbox_tasks.root import ( + InboxTask, + InboxTaskRepository, +) +from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.common.sub.time_events.domain import TimeEventDomain +from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( + TimeEventInDayBlock, +) +from jupiter.core.habits.root import Habit +from jupiter.core.habits.streak_mark import ( + HabitStreakMark, + HabitStreakMarkRepository, +) +from jupiter.core.life_plan.sub.aspects.root import Aspect +from jupiter.core.life_plan.sub.chapters.root import Chapter +from jupiter.core.life_plan.sub.goals.root import Goal +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.adate import ADate +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.time_provider import TimeProvider +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result + + +@use_case_result +class HabitLoadResult(UseCaseResultBase): + """HabitLoadResult.""" + + habit: Habit + aspect: Aspect + chapter: Chapter | None + goal: Goal | None + inbox_tasks: list[InboxTask] + inbox_tasks_total_cnt: int + inbox_tasks_page_size: int + streak_marks: list[HabitStreakMark] + streak_mark_earliest_date: ADate + streak_mark_latest_date: ADate + tags: list[Tag] + contacts: list[Contact] + note: Note | None + time_event_blocks: list[TimeEventInDayBlock] + publish_entity: PublishEntity | None + + +class HabitLoadService: + """Shared service for loading a habit and its dependent entities.""" + + def __init__(self, time_provider: TimeProvider) -> None: + """Constructor.""" + self._time_provider = time_provider + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + habit: Habit, + *, + allow_archived: bool = False, + inbox_task_retrieve_offset: int = 0, + include_streak_marks_earliest_date: ADate | None = None, + include_streak_marks_latest_date: ADate | None = None, + include_publish_entity: bool = True, + ) -> HabitLoadResult: + """Load a habit and its dependent entities.""" + habit = await uow.get_for(Habit).load_by_id( + habit.ref_id, allow_archived=allow_archived + ) + aspect = await uow.get_for(Aspect).load_by_id(habit.aspect_ref_id) + chapter = ( + await uow.get_for(Chapter).load_by_id(habit.chapter_ref_id) + if habit.chapter_ref_id + else None + ) + goal = ( + await uow.get_for(Goal).load_by_id(habit.goal_ref_id) + if habit.goal_ref_id + else None + ) + inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( + workspace_ref_id, + ) + + inbox_tasks_total_cnt = await uow.get(InboxTaskRepository).count_all_for_owner( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=allow_archived, + owner=EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), + ) + inbox_tasks = await uow.get( + InboxTaskRepository + ).find_all_for_owner_created_desc( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=True, + owner=EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), + retrieve_offset=inbox_task_retrieve_offset, + retrieve_limit=InboxTaskRepository.PAGE_SIZE, + ) + + streak_mark_earliest_date = ( + include_streak_marks_earliest_date + or self._time_provider.get_current_date().subtract_days(365) + ) + streak_mark_latest_date = ( + include_streak_marks_latest_date or self._time_provider.get_current_date() + ) + + streak_marks = await uow.get(HabitStreakMarkRepository).find_all_between_dates( + habit.ref_id, + streak_mark_earliest_date, + streak_mark_latest_date, + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace_ref_id, + ) + contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), + ) + if contact_link is not None: + contacts = await uow.get_for(Contact).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + ref_id=contact_link.contacts_ref_ids, + ) + else: + contacts = [] + + note = await uow.get(NoteRepository).load_optional_for_owner( + EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), + allow_archived=allow_archived, + ) + + time_event_domain = await uow.get_for(TimeEventDomain).load_by_parent( + workspace_ref_id + ) + time_event_blocks = await uow.get_for(TimeEventInDayBlock).find_all_generic( + parent_ref_id=time_event_domain.ref_id, + allow_archived=False, + owner=EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), + ) + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), + allow_archived=allow_archived, + ) + + return HabitLoadResult( + habit=habit, + aspect=aspect, + chapter=chapter, + goal=goal, + inbox_tasks=inbox_tasks, + inbox_tasks_total_cnt=inbox_tasks_total_cnt, + inbox_tasks_page_size=InboxTaskRepository.PAGE_SIZE, + streak_marks=streak_marks, + streak_mark_earliest_date=streak_mark_earliest_date, + streak_mark_latest_date=streak_mark_latest_date, + tags=tags, + contacts=contacts, + note=note, + time_event_blocks=time_event_blocks, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/habits/use_case/load.py b/src/core/jupiter/core/habits/use_case/load.py index 8abd73a0c..556ff810b 100644 --- a/src/core/jupiter/core/habits/use_case/load.py +++ b/src/core/jupiter/core/habits/use_case/load.py @@ -1,39 +1,14 @@ """Use case for loading a particular habit.""" -from jupiter.core.common.sub.contacts.root import ContactDomain -from jupiter.core.common.sub.contacts.sub.contact.root import Contact -from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository -from jupiter.core.common.sub.inbox_tasks.collection import ( - InboxTaskCollection, -) -from jupiter.core.common.sub.inbox_tasks.root import ( - InboxTask, - InboxTaskRepository, -) -from jupiter.core.common.sub.notes.root import Note, NoteRepository -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository -from jupiter.core.common.sub.time_events.domain import TimeEventDomain -from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( - TimeEventInDayBlock, -) from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature from jupiter.core.habits.root import Habit -from jupiter.core.habits.streak_mark import ( - HabitStreakMark, - HabitStreakMarkRepository, -) -from jupiter.core.life_plan.sub.aspects.root import Aspect -from jupiter.core.life_plan.sub.chapters.root import Chapter -from jupiter.core.life_plan.sub.goals.root import Goal -from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.habits.service.load import HabitLoadResult, HabitLoadService from jupiter.framework.base.adate import ADate from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.errors import InputValidationError from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import ( @@ -41,11 +16,11 @@ ) from jupiter.framework.use_case_io import ( UseCaseArgsBase, - UseCaseResultBase, use_case_args, - use_case_result, ) +__all__ = ["HabitLoadArgs", "HabitLoadResult", "HabitLoadUseCase"] + @use_case_args class HabitLoadArgs(UseCaseArgsBase): @@ -58,26 +33,6 @@ class HabitLoadArgs(UseCaseArgsBase): include_streak_marks_latest_date: ADate | None -@use_case_result -class HabitLoadResult(UseCaseResultBase): - """HabitLoadResult.""" - - habit: Habit - aspect: Aspect - chapter: Chapter | None - goal: Goal | None - inbox_tasks: list[InboxTask] - inbox_tasks_total_cnt: int - inbox_tasks_page_size: int - streak_marks: list[HabitStreakMark] - streak_mark_earliest_date: ADate - streak_mark_latest_date: ADate - tags: list[Tag] - contacts: list[Contact] - note: Note | None - time_event_blocks: list[TimeEventInDayBlock] - - @readonly_use_case(WorkspaceFeature.HABITS) class HabitLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[HabitLoadArgs, HabitLoadResult] @@ -112,104 +67,13 @@ async def _perform_transactional_read( habit = await uow.get_for(Habit).load_by_id( args.ref_id, allow_archived=allow_archived ) - aspect = await uow.get_for(Aspect).load_by_id(habit.aspect_ref_id) - chapter = ( - await uow.get_for(Chapter).load_by_id(habit.chapter_ref_id) - if habit.chapter_ref_id - else None - ) - goal = ( - await uow.get_for(Goal).load_by_id(habit.goal_ref_id) - if habit.goal_ref_id - else None - ) - inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( - workspace.ref_id, - ) - - inbox_tasks_total_cnt = await uow.get(InboxTaskRepository).count_all_for_owner( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=allow_archived, - owner=EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), - ) - inbox_tasks = await uow.get( - InboxTaskRepository - ).find_all_for_owner_created_desc( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=True, - owner=EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), - retrieve_offset=args.inbox_task_retrieve_offset or 0, - retrieve_limit=InboxTaskRepository.PAGE_SIZE, - ) - streak_mark_earliest_date = ( - args.include_streak_marks_earliest_date - or self._time_provider.get_current_date().subtract_days(365) - ) - streak_mark_latest_date = ( - args.include_streak_marks_latest_date - or self._time_provider.get_current_date() - ) - - streak_marks = await uow.get(HabitStreakMarkRepository).find_all_between_dates( - habit.ref_id, - streak_mark_earliest_date, - streak_mark_latest_date, - ) - - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - contact_domain = await uow.get_for(ContactDomain).load_by_parent( + return await HabitLoadService(self._time_provider).do_it( + uow, workspace.ref_id, - ) - contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), - ) - if contact_link is not None: - contacts = await uow.get_for(Contact).find_all_generic( - parent_ref_id=contact_domain.ref_id, - allow_archived=False, - ref_id=contact_link.contacts_ref_ids, - ) - else: - contacts = [] - - note = await uow.get(NoteRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), + habit, allow_archived=allow_archived, - ) - - time_event_domain = await uow.get_for(TimeEventDomain).load_by_parent( - workspace.ref_id - ) - time_event_blocks = await uow.get_for(TimeEventInDayBlock).find_all_generic( - parent_ref_id=time_event_domain.ref_id, - allow_archived=False, - owner=EntityLink.std(NamedEntityTag.HABIT.value, habit.ref_id), - ) - - return HabitLoadResult( - habit=habit, - aspect=aspect, - chapter=chapter, - goal=goal, - inbox_tasks=inbox_tasks, - inbox_tasks_total_cnt=inbox_tasks_total_cnt, - inbox_tasks_page_size=InboxTaskRepository.PAGE_SIZE, - streak_marks=streak_marks, - streak_mark_earliest_date=streak_mark_earliest_date, - streak_mark_latest_date=streak_mark_latest_date, - tags=tags, - contacts=contacts, - note=note, - time_event_blocks=time_event_blocks, + inbox_task_retrieve_offset=args.inbox_task_retrieve_offset or 0, + include_streak_marks_earliest_date=args.include_streak_marks_earliest_date, + include_streak_marks_latest_date=args.include_streak_marks_latest_date, ) diff --git a/src/core/jupiter/core/habits/use_case/load_public.py b/src/core/jupiter/core/habits/use_case/load_public.py new file mode 100644 index 000000000..c76758bd5 --- /dev/null +++ b/src/core/jupiter/core/habits/use_case/load_public.py @@ -0,0 +1,99 @@ +"""Guest readonly use case for loading a published habit.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.habits.collection import HabitCollection +from jupiter.core.habits.root import Habit +from jupiter.core.habits.service.load import HabitLoadResult, HabitLoadService +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.adate import ADate +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class HabitLoadPublicArgs(UseCaseArgsBase): + """HabitLoadPublic args.""" + + external_id: PublishExternalId + inbox_task_retrieve_offset: int | None + include_streak_marks_earliest_date: ADate | None + include_streak_marks_latest_date: ADate | None + + +class HabitLoadPublicUseCase( + JupiterGuestReadonlyUseCase[HabitLoadPublicArgs, HabitLoadResult] +): + """Load a published habit by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: HabitLoadPublicArgs, + ) -> HabitLoadResult: + """Execute the use case.""" + if ( + args.inbox_task_retrieve_offset is not None + and args.inbox_task_retrieve_offset < 0 + ): + raise InputValidationError("Invalid inbox_task_retrieve_offset") + if ( + args.include_streak_marks_earliest_date is not None + and args.include_streak_marks_latest_date is not None + and args.include_streak_marks_earliest_date + > args.include_streak_marks_latest_date + ): + raise InputValidationError( + "Invalid streak_mark_earliest_date or streak_mark_latest_date" + ) + + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.HABIT.value: + raise InputValidationError( + "The publish entity does not refer to a habit." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + habit_collection = await uow.get_for(HabitCollection).load_by_parent( + publish_domain.workspace.ref_id + ) + habit = await uow.get_for(Habit).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if habit.parent_ref_id != habit_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace habit." + ) + + return await HabitLoadService(self._time_provider).do_it( + uow, + publish_domain.workspace.ref_id, + habit, + allow_archived=False, + inbox_task_retrieve_offset=args.inbox_task_retrieve_offset or 0, + include_streak_marks_earliest_date=args.include_streak_marks_earliest_date, + include_streak_marks_latest_date=args.include_streak_marks_latest_date, + include_publish_entity=False, + ) diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx index 984d7506d..59ef60ff1 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -37,6 +37,10 @@ function publishedEntityLocation(externalId: string, owner: string): string { return `/app/public/published/doc/${externalId}`; case NamedEntityTag.PERSON: return `/app/public/published/person/${externalId}`; + case NamedEntityTag.HABIT: + return `/app/public/published/habit/${externalId}`; + case NamedEntityTag.CHORE: + return `/app/public/published/chore/${externalId}`; default: throw new Response(ReasonPhrases.NOT_FOUND, { status: StatusCodes.NOT_FOUND, diff --git a/src/webui/app/routes/app/public/published/chore/$externalId.tsx b/src/webui/app/routes/app/public/published/chore/$externalId.tsx new file mode 100644 index 000000000..0e4524a61 --- /dev/null +++ b/src/webui/app/routes/app/public/published/chore/$externalId.tsx @@ -0,0 +1,149 @@ +import type { InboxTask } from "@jupiter/webapi-client"; +import { ApiError } from "@jupiter/webapi-client"; +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext, useMemo } from "react"; +import { z } from "zod"; +import { parseParams, parseQuery } from "zodix"; +import { sortInboxTasksNaturally } from "#/core/common/sub/inbox_tasks/root"; +import { InboxTaskStack } from "#/core/common/sub/inbox_tasks/component/stack"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { ChoreEditor } from "@jupiter/core/chores/component/editor"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +const QuerySchema = z.object({ + inboxTasksRetrieveOffset: z + .string() + .transform((s) => parseInt(s, 10)) + .optional(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const query = parseQuery(request, QuerySchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.chores.choreLoadPublic({ + external_id: externalId, + inbox_task_retrieve_offset: query.inboxTasksRetrieveOffset, + }); + + return json({ + chore: result.chore, + tags: result.tags ?? [], + contacts: result.contacts ?? [], + note: result.note ?? null, + aspect: result.aspect, + chapter: result.chapter ?? null, + goal: result.goal ?? null, + inboxTasks: result.inbox_tasks as Array<InboxTask>, + inboxTasksTotalCnt: result.inbox_tasks_total_cnt, + inboxTasksPageSize: result.inbox_tasks_page_size, + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedChore() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + + const sortedInboxTasks = useMemo( + () => + sortInboxTasksNaturally(loaderData.inboxTasks, { + dueDateAscending: false, + }), + [loaderData.inboxTasks], + ); + + return ( + <LeafPanel + key={`published-chore-${loaderData.chore.ref_id}`} + fakeKey={`published-chore-${loaderData.chore.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <ChoreEditor + chore={loaderData.chore} + tags={loaderData.tags} + contacts={loaderData.contacts} + allTags={loaderData.tags} + allContacts={loaderData.contacts} + aspect={loaderData.aspect} + chapter={loaderData.chapter} + goal={loaderData.goal} + inputsEnabled={false} + topLevelInfo={topLevelInfo} + /> + + <SectionCard title="Note"> + {loaderData.note ? ( + <EntityNoteEditor + initialNote={loaderData.note} + inputsEnabled={false} + /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + + <SectionCard title="Inbox Tasks"> + {sortedInboxTasks.length > 0 && ( + <InboxTaskStack + topLevelInfo={topLevelInfo} + showOptions={{ + showStatus: true, + showDueDate: true, + }} + inboxTasks={sortedInboxTasks} + linksEnabled={false} + withPages={{ + retrieveOffsetParamName: "inboxTasksRetrieveOffset", + totalCnt: loaderData.inboxTasksTotalCnt, + pageSize: loaderData.inboxTasksPageSize, + }} + /> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => `Could not find published chore ${params.externalId}!`, + error: (params) => + `There was an error loading published chore ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/public/published/habit/$externalId.tsx b/src/webui/app/routes/app/public/published/habit/$externalId.tsx new file mode 100644 index 000000000..982cba85a --- /dev/null +++ b/src/webui/app/routes/app/public/published/habit/$externalId.tsx @@ -0,0 +1,168 @@ +import type { InboxTask } from "@jupiter/webapi-client"; +import { ApiError } from "@jupiter/webapi-client"; +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { DateTime } from "luxon"; +import { useContext, useMemo } from "react"; +import { z } from "zod"; +import { parseParams, parseQuery } from "zodix"; +import { sortInboxTasksNaturally } from "#/core/common/sub/inbox_tasks/root"; +import { InboxTaskStack } from "#/core/common/sub/inbox_tasks/component/stack"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { HabitEditor } from "@jupiter/core/habits/component/editor"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +const QuerySchema = z.object({ + inboxTasksRetrieveOffset: z + .string() + .transform((s) => parseInt(s, 10)) + .optional(), + viewOneIncludeStreakMarksEarliestDate: z.string().optional(), + viewOneIncludeStreakMarksLatestDate: z.string().optional(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const query = parseQuery(request, QuerySchema); + const apiClient = await getGuestApiClient(request); + + let earliestDate = query.viewOneIncludeStreakMarksEarliestDate; + let latestDate = query.viewOneIncludeStreakMarksLatestDate; + if (earliestDate === undefined) { + earliestDate = DateTime.now().minus({ days: 90 }).toISODate() ?? undefined; + latestDate = DateTime.now().toISODate() ?? undefined; + } + + try { + const result = await apiClient.habits.habitLoadPublic({ + external_id: externalId, + inbox_task_retrieve_offset: query.inboxTasksRetrieveOffset, + include_streak_marks_earliest_date: earliestDate, + include_streak_marks_latest_date: latestDate, + }); + + return json({ + habit: result.habit, + tags: result.tags ?? [], + contacts: result.contacts ?? [], + note: result.note ?? null, + aspect: result.aspect, + chapter: result.chapter ?? null, + goal: result.goal ?? null, + inboxTasks: result.inbox_tasks as Array<InboxTask>, + inboxTasksTotalCnt: result.inbox_tasks_total_cnt, + inboxTasksPageSize: result.inbox_tasks_page_size, + streakMarks: result.streak_marks, + streakMarkEarliestDate: result.streak_mark_earliest_date, + streakMarkLatestDate: result.streak_mark_latest_date, + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedHabit() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + + const sortedInboxTasks = useMemo( + () => + sortInboxTasksNaturally(loaderData.inboxTasks, { + dueDateAscending: false, + }), + [loaderData.inboxTasks], + ); + + return ( + <LeafPanel + key={`published-habit-${loaderData.habit.ref_id}`} + fakeKey={`published-habit-${loaderData.habit.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <HabitEditor + habit={loaderData.habit} + tags={loaderData.tags} + contacts={loaderData.contacts} + allTags={loaderData.tags} + allContacts={loaderData.contacts} + aspect={loaderData.aspect} + chapter={loaderData.chapter} + goal={loaderData.goal} + inputsEnabled={false} + topLevelInfo={topLevelInfo} + streakMarks={loaderData.streakMarks} + streakMarkEarliestDate={loaderData.streakMarkEarliestDate} + streakMarkLatestDate={loaderData.streakMarkLatestDate} + showStreak + /> + + <SectionCard title="Note"> + {loaderData.note ? ( + <EntityNoteEditor + initialNote={loaderData.note} + inputsEnabled={false} + /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + + <SectionCard title="Inbox Tasks"> + {sortedInboxTasks.length > 0 && ( + <InboxTaskStack + topLevelInfo={topLevelInfo} + showOptions={{ + showStatus: true, + showDueDate: true, + }} + inboxTasks={sortedInboxTasks} + linksEnabled={false} + withPages={{ + retrieveOffsetParamName: "inboxTasksRetrieveOffset", + totalCnt: loaderData.inboxTasksTotalCnt, + pageSize: loaderData.inboxTasksPageSize, + }} + /> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => `Could not find published habit ${params.externalId}!`, + error: (params) => + `There was an error loading published habit ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/workspace/chores/$id.tsx b/src/webui/app/routes/app/workspace/chores/$id.tsx index 649d67c33..716725527 100644 --- a/src/webui/app/routes/app/workspace/chores/$id.tsx +++ b/src/webui/app/routes/app/workspace/chores/$id.tsx @@ -111,6 +111,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -169,6 +181,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ).contacts ?? [], allContacts: allContacts.contacts as Array<Contact>, timeEventBlocks: result.time_event_blocks, + publishEntity: result.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -328,6 +341,30 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/chores`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/chores/${id}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/chores/${id}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/chores/${id}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -339,6 +376,10 @@ export async function action({ request, params }: ActionFunctionArgs) { return json(validationErrorToUIErrorInfo(error.body)); } + if (error instanceof ApiError && error.status === StatusCodes.CONFLICT) { + return json(validationErrorToUIErrorInfo(error.body)); + } + throw error; } } @@ -425,6 +466,8 @@ export default function Chore() { inputsEnabled={inputsEnabled} entityArchived={loaderData.chore.archived} returnLocation="/app/workspace/chores" + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <GlobalError actionResult={actionData} /> <SectionCard diff --git a/src/webui/app/routes/app/workspace/habits/$id.tsx b/src/webui/app/routes/app/workspace/habits/$id.tsx index 8a57c4e17..6c2ea344b 100644 --- a/src/webui/app/routes/app/workspace/habits/$id.tsx +++ b/src/webui/app/routes/app/workspace/habits/$id.tsx @@ -118,6 +118,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -188,6 +200,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ).contacts ?? [], allContacts: allContacts.contacts as Array<Contact>, timeEventBlocks: result.time_event_blocks, + publishEntity: result.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -343,6 +356,30 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/habits`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/habits/${id}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/habits/${id}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/habits/${id}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -354,6 +391,10 @@ export async function action({ request, params }: ActionFunctionArgs) { return json(validationErrorToUIErrorInfo(error.body)); } + if (error instanceof ApiError && error.status === StatusCodes.CONFLICT) { + return json(validationErrorToUIErrorInfo(error.body)); + } + throw error; } } @@ -452,6 +493,8 @@ export default function Habit() { entityArchived={loaderData.habit.archived} returnLocation="/app/workspace/habits" initialExpansionState={LeafPanelExpansionState.MEDIUM} + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <GlobalError actionResult={actionData} /> <SectionCard From 63a01933eef65f77350b646963e5e6b10aa9e29a Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Mon, 8 Jun 2026 10:51:32 +0300 Subject: [PATCH 09/21] Added big plans --- .../api/big_plans/big_plan_load_public.py | 212 ++++++++++++++++++ .../jupiter_webapi_client/models/__init__.py | 2 + .../models/big_plan_load_public_args.py | 84 +++++++ .../models/big_plan_load_result.py | 49 ++++ gen/ts/webapi-client/gen/index.ts | 1 + .../gen/models/BigPlanLoadPublicArgs.ts | 13 ++ .../gen/models/BigPlanLoadResult.ts | 4 + .../gen/services/BigPlansService.ts | 29 +++ itests/webui/entities/big_plans.test.py | 34 ++- src/core/jupiter/core/big_plans/root.py | 4 + .../jupiter/core/big_plans/service/load.py | 190 ++++++++++++++++ .../sub/milestones/component/stack.tsx | 30 ++- .../jupiter/core/big_plans/use_case/load.py | 130 +---------- .../core/big_plans/use_case/load_public.py | 86 +++++++ .../common/sub/publish/sub/entity/root.py | 2 +- .../app/public/published/$externalId.tsx | 2 + .../public/published/big-plan/$externalId.tsx | 186 +++++++++++++++ .../routes/app/workspace/big-plans/$id.tsx | 43 ++++ 18 files changed, 968 insertions(+), 133 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/big_plans/big_plan_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/big_plan_load_public_args.py create mode 100644 gen/ts/webapi-client/gen/models/BigPlanLoadPublicArgs.ts create mode 100644 src/core/jupiter/core/big_plans/service/load.py create mode 100644 src/core/jupiter/core/big_plans/use_case/load_public.py create mode 100644 src/webui/app/routes/app/public/published/big-plan/$externalId.tsx diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/big_plans/big_plan_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/big_plans/big_plan_load_public.py new file mode 100644 index 000000000..460313463 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/big_plans/big_plan_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.big_plan_load_public_args import BigPlanLoadPublicArgs +from ...models.big_plan_load_result import BigPlanLoadResult +from ...models.error_response import ErrorResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: BigPlanLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/big-plan-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> BigPlanLoadResult | ErrorResponse | None: + if response.status_code == 200: + response_200 = BigPlanLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[BigPlanLoadResult | ErrorResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: BigPlanLoadPublicArgs | Unset = UNSET, +) -> Response[BigPlanLoadResult | ErrorResponse]: + """Load a published big plan by publish external id. + + Args: + body (BigPlanLoadPublicArgs | Unset): BigPlanLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[BigPlanLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: BigPlanLoadPublicArgs | Unset = UNSET, +) -> BigPlanLoadResult | ErrorResponse | None: + """Load a published big plan by publish external id. + + Args: + body (BigPlanLoadPublicArgs | Unset): BigPlanLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + BigPlanLoadResult | ErrorResponse + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: BigPlanLoadPublicArgs | Unset = UNSET, +) -> Response[BigPlanLoadResult | ErrorResponse]: + """Load a published big plan by publish external id. + + Args: + body (BigPlanLoadPublicArgs | Unset): BigPlanLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[BigPlanLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: BigPlanLoadPublicArgs | Unset = UNSET, +) -> BigPlanLoadResult | ErrorResponse | None: + """Load a published big plan by publish external id. + + Args: + body (BigPlanLoadPublicArgs | Unset): BigPlanLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + BigPlanLoadResult | ErrorResponse + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index ad635d64f..a47cf1ca2 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -65,6 +65,7 @@ from .big_plan_find_result import BigPlanFindResult from .big_plan_find_result_entry import BigPlanFindResultEntry from .big_plan_load_args import BigPlanLoadArgs +from .big_plan_load_public_args import BigPlanLoadPublicArgs from .big_plan_load_result import BigPlanLoadResult from .big_plan_milestone import BigPlanMilestone from .big_plan_milestone_archive_args import BigPlanMilestoneArchiveArgs @@ -1087,6 +1088,7 @@ "BigPlanFindResult", "BigPlanFindResultEntry", "BigPlanLoadArgs", + "BigPlanLoadPublicArgs", "BigPlanLoadResult", "BigPlanMilestone", "BigPlanMilestoneArchiveArgs", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/big_plan_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/big_plan_load_public_args.py new file mode 100644 index 000000000..244304289 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/big_plan_load_public_args.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="BigPlanLoadPublicArgs") + + +@_attrs_define +class BigPlanLoadPublicArgs: + """BigPlanLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + inbox_task_retrieve_offset (int | None | Unset): + """ + + external_id: str + inbox_task_retrieve_offset: int | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + inbox_task_retrieve_offset: int | None | Unset + if isinstance(self.inbox_task_retrieve_offset, Unset): + inbox_task_retrieve_offset = UNSET + else: + inbox_task_retrieve_offset = self.inbox_task_retrieve_offset + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + if inbox_task_retrieve_offset is not UNSET: + field_dict["inbox_task_retrieve_offset"] = inbox_task_retrieve_offset + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + def _parse_inbox_task_retrieve_offset(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + inbox_task_retrieve_offset = _parse_inbox_task_retrieve_offset(d.pop("inbox_task_retrieve_offset", UNSET)) + + big_plan_load_public_args = cls( + external_id=external_id, + inbox_task_retrieve_offset=inbox_task_retrieve_offset, + ) + + big_plan_load_public_args.additional_properties = d + return big_plan_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/big_plan_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/big_plan_load_result.py index 59219c80d..c21400381 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/big_plan_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/big_plan_load_result.py @@ -18,6 +18,7 @@ from ..models.goal import Goal from ..models.inbox_task import InboxTask from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_in_day_block import TimeEventInDayBlock @@ -34,6 +35,8 @@ class BigPlanLoadResult: aspect (Aspect): The aspect. milestones (list[BigPlanMilestone]): inbox_tasks (list[InboxTask]): + inbox_tasks_total_cnt (int): + inbox_tasks_page_size (int): tags (list[Tag]): contacts (list[Contact]): time_event_blocks (list[TimeEventInDayBlock]): @@ -41,12 +44,15 @@ class BigPlanLoadResult: chapter (Chapter | None | Unset): goal (Goal | None | Unset): note (None | Note | Unset): + publish_entity (None | PublishEntity | Unset): """ big_plan: BigPlan aspect: Aspect milestones: list[BigPlanMilestone] inbox_tasks: list[InboxTask] + inbox_tasks_total_cnt: int + inbox_tasks_page_size: int tags: list[Tag] contacts: list[Contact] time_event_blocks: list[TimeEventInDayBlock] @@ -54,12 +60,14 @@ class BigPlanLoadResult: chapter: Chapter | None | Unset = UNSET goal: Goal | None | Unset = UNSET note: None | Note | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.chapter import Chapter from ..models.goal import Goal from ..models.note import Note + from ..models.publish_entity import PublishEntity big_plan = self.big_plan.to_dict() @@ -75,6 +83,10 @@ def to_dict(self) -> dict[str, Any]: inbox_tasks_item = inbox_tasks_item_data.to_dict() inbox_tasks.append(inbox_tasks_item) + inbox_tasks_total_cnt = self.inbox_tasks_total_cnt + + inbox_tasks_page_size = self.inbox_tasks_page_size + tags = [] for tags_item_data in self.tags: tags_item = tags_item_data.to_dict() @@ -116,6 +128,14 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -124,6 +144,8 @@ def to_dict(self) -> dict[str, Any]: "aspect": aspect, "milestones": milestones, "inbox_tasks": inbox_tasks, + "inbox_tasks_total_cnt": inbox_tasks_total_cnt, + "inbox_tasks_page_size": inbox_tasks_page_size, "tags": tags, "contacts": contacts, "time_event_blocks": time_event_blocks, @@ -136,6 +158,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["goal"] = goal if note is not UNSET: field_dict["note"] = note + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -150,6 +174,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.goal import Goal from ..models.inbox_task import InboxTask from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag from ..models.time_event_in_day_block import TimeEventInDayBlock @@ -172,6 +197,10 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: inbox_tasks.append(inbox_tasks_item) + inbox_tasks_total_cnt = d.pop("inbox_tasks_total_cnt") + + inbox_tasks_page_size = d.pop("inbox_tasks_page_size") + tags = [] _tags = d.pop("tags") for tags_item_data in _tags: @@ -246,11 +275,30 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + big_plan_load_result = cls( big_plan=big_plan, aspect=aspect, milestones=milestones, inbox_tasks=inbox_tasks, + inbox_tasks_total_cnt=inbox_tasks_total_cnt, + inbox_tasks_page_size=inbox_tasks_page_size, tags=tags, contacts=contacts, time_event_blocks=time_event_blocks, @@ -258,6 +306,7 @@ def _parse_note(data: object) -> None | Note | Unset: chapter=chapter, goal=goal, note=note, + publish_entity=publish_entity, ) big_plan_load_result.additional_properties = d diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index dd7278d66..7188f8c53 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -63,6 +63,7 @@ export type { BigPlanFindArgs } from './models/BigPlanFindArgs'; export type { BigPlanFindResult } from './models/BigPlanFindResult'; export type { BigPlanFindResultEntry } from './models/BigPlanFindResultEntry'; export type { BigPlanLoadArgs } from './models/BigPlanLoadArgs'; +export type { BigPlanLoadPublicArgs } from './models/BigPlanLoadPublicArgs'; export type { BigPlanLoadResult } from './models/BigPlanLoadResult'; export type { BigPlanMilestone } from './models/BigPlanMilestone'; export type { BigPlanMilestoneArchiveArgs } from './models/BigPlanMilestoneArchiveArgs'; diff --git a/gen/ts/webapi-client/gen/models/BigPlanLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/BigPlanLoadPublicArgs.ts new file mode 100644 index 000000000..35149668a --- /dev/null +++ b/gen/ts/webapi-client/gen/models/BigPlanLoadPublicArgs.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * BigPlanLoadPublic args. + */ +export type BigPlanLoadPublicArgs = { + external_id: PublishExternalId; + inbox_task_retrieve_offset?: (number | null); +}; + diff --git a/gen/ts/webapi-client/gen/models/BigPlanLoadResult.ts b/gen/ts/webapi-client/gen/models/BigPlanLoadResult.ts index 163e6b153..aa1ad7448 100644 --- a/gen/ts/webapi-client/gen/models/BigPlanLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/BigPlanLoadResult.ts @@ -11,6 +11,7 @@ import type { Contact } from './Contact'; import type { Goal } from './Goal'; import type { InboxTask } from './InboxTask'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { Tag } from './Tag'; import type { TimeEventInDayBlock } from './TimeEventInDayBlock'; /** @@ -23,10 +24,13 @@ export type BigPlanLoadResult = { goal?: (Goal | null); milestones: Array<BigPlanMilestone>; inbox_tasks: Array<InboxTask>; + inbox_tasks_total_cnt: number; + inbox_tasks_page_size: number; tags: Array<Tag>; contacts: Array<Contact>; note?: (Note | null); time_event_blocks: Array<TimeEventInDayBlock>; stats: BigPlanStats; + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/services/BigPlansService.ts b/gen/ts/webapi-client/gen/services/BigPlansService.ts index e86a5f34d..08443a4f5 100644 --- a/gen/ts/webapi-client/gen/services/BigPlansService.ts +++ b/gen/ts/webapi-client/gen/services/BigPlansService.ts @@ -10,6 +10,7 @@ import type { BigPlanCreateResult } from '../models/BigPlanCreateResult'; import type { BigPlanFindArgs } from '../models/BigPlanFindArgs'; import type { BigPlanFindResult } from '../models/BigPlanFindResult'; import type { BigPlanLoadArgs } from '../models/BigPlanLoadArgs'; +import type { BigPlanLoadPublicArgs } from '../models/BigPlanLoadPublicArgs'; import type { BigPlanLoadResult } from '../models/BigPlanLoadResult'; import type { BigPlanMilestoneArchiveArgs } from '../models/BigPlanMilestoneArchiveArgs'; import type { BigPlanMilestoneCreateArgs } from '../models/BigPlanMilestoneCreateArgs'; @@ -306,6 +307,34 @@ export class BigPlansService { }, }); } + /** + * Load a published big plan by publish external id. + * @param requestBody The input data + * @returns BigPlanLoadResult Successful response + * @throws ApiError + */ + public bigPlanLoadPublic( + requestBody?: BigPlanLoadPublicArgs, + ): CancelablePromise<BigPlanLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/big-plan-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * A use case for refreshing stats for a big plan. * @param requestBody The input data diff --git a/itests/webui/entities/big_plans.test.py b/itests/webui/entities/big_plans.test.py index d951f80c0..4d6e15610 100644 --- a/itests/webui/entities/big_plans.test.py +++ b/itests/webui/entities/big_plans.test.py @@ -1,5 +1,7 @@ """Tests about big plans.""" +import re + import pytest from jupiter_webapi_client.api.big_plans.big_plan_create import ( sync_detailed as big_plan_create_sync, @@ -23,7 +25,7 @@ ) from playwright.sync_api import Page, expect -from itests.helpers import get_parsed_from_response +from itests.helpers import get_parsed_from_response, open_leaf_publish_panel @pytest.fixture(autouse=True, scope="module") @@ -104,3 +106,33 @@ def test_webui_big_plan_view_all(page: Page, create_big_plan) -> None: expect(page.locator(f"#big-plan-{big_plan1.ref_id}")).to_contain_text("Big Plan 1") expect(page.locator(f"#big-plan-{big_plan2.ref_id}")).to_contain_text("Big Plan 2") expect(page.locator(f"#big-plan-{big_plan3.ref_id}")).to_contain_text("Big Plan 3") + + +def test_webui_big_plan_publish_and_view_public(page: Page, create_big_plan) -> None: + big_plan = create_big_plan("Published Big Plan") + page.goto(f"/app/workspace/big-plans/{big_plan.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "BigPlan-publish") + page.locator("button[id='BigPlan-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/big-plans/{big_plan.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "BigPlan-publish") + expect(page.locator("#BigPlan-publish")).to_contain_text("draft") + + page.locator("button[id='BigPlan-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/big-plans/{big_plan.ref_id}")) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "BigPlan-publish") + expect(page.locator("#BigPlan-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/big-plan/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="name"]')).to_have_value("Published Big Plan") diff --git a/src/core/jupiter/core/big_plans/root.py b/src/core/jupiter/core/big_plans/root.py index 8a1b06bee..515771a9b 100644 --- a/src/core/jupiter/core/big_plans/root.py +++ b/src/core/jupiter/core/big_plans/root.py @@ -12,6 +12,7 @@ from jupiter.core.common.eisen import Eisen from jupiter.core.common.sub.inbox_tasks.root import InboxTask from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( TimeEventInDayBlock, @@ -70,6 +71,9 @@ class BigPlan(LeafEntity): TagLink, owner=IsEntityLinkStd(NamedEntityTag.BIG_PLAN.value) ) note = OwnsAtMostOne(Note, owner=IsEntityLinkStd(NamedEntityTag.BIG_PLAN.value)) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.BIG_PLAN.value) + ) stats = ContainsOneRecord(BigPlanStats, big_plan_ref_id=IsRefId()) @staticmethod diff --git a/src/core/jupiter/core/big_plans/service/load.py b/src/core/jupiter/core/big_plans/service/load.py new file mode 100644 index 000000000..d76dde25d --- /dev/null +++ b/src/core/jupiter/core/big_plans/service/load.py @@ -0,0 +1,190 @@ +"""Shared service for loading a big plan and its dependent entities.""" + +from jupiter.core.big_plans.root import BigPlan +from jupiter.core.big_plans.stats import BigPlanStats, BigPlanStatsRepository +from jupiter.core.big_plans.sub.milestones.root import BigPlanMilestone +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.inbox_tasks.collection import InboxTaskCollection +from jupiter.core.common.sub.inbox_tasks.root import ( + InboxTask, + InboxTaskRepository, +) +from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.common.sub.time_events.domain import TimeEventDomain +from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( + TimeEventInDayBlock, +) +from jupiter.core.life_plan.sub.aspects.root import Aspect +from jupiter.core.life_plan.sub.chapters.root import Chapter +from jupiter.core.life_plan.sub.goals.root import Goal +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result + + +@use_case_result +class BigPlanLoadResult(UseCaseResultBase): + """BigPlanLoadResult.""" + + big_plan: BigPlan + aspect: Aspect + chapter: Chapter | None + goal: Goal | None + milestones: list[BigPlanMilestone] + inbox_tasks: list[InboxTask] + inbox_tasks_total_cnt: int + inbox_tasks_page_size: int + tags: list[Tag] + contacts: list[Contact] + note: Note | None + time_event_blocks: list[TimeEventInDayBlock] + stats: BigPlanStats + publish_entity: PublishEntity | None + + +class BigPlanLoadService: + """Shared service for loading a big plan and its dependent entities.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + big_plan: BigPlan, + *, + allow_archived: bool = False, + inbox_task_retrieve_offset: int = 0, + paginate_inbox_tasks: bool = False, + include_publish_entity: bool = True, + ) -> BigPlanLoadResult: + """Load a big plan and its dependent entities.""" + big_plan = await uow.get_for(BigPlan).load_by_id( + big_plan.ref_id, allow_archived=allow_archived + ) + aspect = await uow.get_for(Aspect).load_by_id(big_plan.aspect_ref_id) + chapter = ( + await uow.get_for(Chapter).load_by_id(big_plan.chapter_ref_id) + if big_plan.chapter_ref_id + else None + ) + goal = ( + await uow.get_for(Goal).load_by_id(big_plan.goal_ref_id) + if big_plan.goal_ref_id + else None + ) + milestones = await uow.get_for(BigPlanMilestone).find_all_generic( + parent_ref_id=big_plan.ref_id, + allow_archived=False, + ) + inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( + workspace_ref_id, + ) + owner = EntityLink.std(NamedEntityTag.BIG_PLAN.value, big_plan.ref_id) + + if paginate_inbox_tasks: + inbox_tasks_total_cnt = await uow.get( + InboxTaskRepository + ).count_all_for_owner( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=allow_archived, + owner=owner, + ) + inbox_tasks = await uow.get( + InboxTaskRepository + ).find_all_for_owner_created_desc( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=True, + owner=owner, + retrieve_offset=inbox_task_retrieve_offset, + retrieve_limit=InboxTaskRepository.PAGE_SIZE, + ) + inbox_tasks_page_size = InboxTaskRepository.PAGE_SIZE + else: + inbox_tasks = await uow.get( + InboxTaskRepository + ).find_all_for_owner_created_desc( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=allow_archived, + owner=owner, + ) + inbox_tasks_total_cnt = len(inbox_tasks) + inbox_tasks_page_size = max(inbox_tasks_total_cnt, 1) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=owner, + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace_ref_id, + ) + contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( + owner, + ) + if contact_link is not None: + contacts = await uow.get_for(Contact).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + ref_id=contact_link.contacts_ref_ids, + ) + else: + contacts = [] + + note = await uow.get(NoteRepository).load_optional_for_owner( + owner, + allow_archived=allow_archived, + ) + time_event_domain = await uow.get_for(TimeEventDomain).load_by_parent( + workspace_ref_id + ) + time_event_blocks = await uow.get_for(TimeEventInDayBlock).find_all_generic( + parent_ref_id=time_event_domain.ref_id, + allow_archived=False, + owner=owner, + ) + stats = await uow.get(BigPlanStatsRepository).load_by_key_optional( + big_plan.ref_id + ) + if stats is None: + raise Exception("Stats not found") + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + owner, + allow_archived=allow_archived, + ) + + return BigPlanLoadResult( + big_plan=big_plan, + aspect=aspect, + chapter=chapter, + goal=goal, + milestones=milestones, + inbox_tasks=inbox_tasks, + inbox_tasks_total_cnt=inbox_tasks_total_cnt, + inbox_tasks_page_size=inbox_tasks_page_size, + tags=tags, + contacts=contacts, + note=note, + time_event_blocks=time_event_blocks, + stats=stats, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/big_plans/sub/milestones/component/stack.tsx b/src/core/jupiter/core/big_plans/sub/milestones/component/stack.tsx index 2c2cbd8a7..e9f28136c 100644 --- a/src/core/jupiter/core/big_plans/sub/milestones/component/stack.tsx +++ b/src/core/jupiter/core/big_plans/sub/milestones/component/stack.tsx @@ -1,32 +1,48 @@ import { type BigPlanMilestone } from "@jupiter/webapi-client"; import { sortBigPlanMilestones } from "#/core/big_plans/root"; -import { EntityCard, EntityLink } from "#/core/infra/component/entity-card"; +import { + EntityCard, + EntityFakeLink, + EntityLink, +} from "#/core/infra/component/entity-card"; import { EntityNameComponent } from "#/core/common/component/entity-name"; import { EntityStack } from "#/core/infra/component/entity-stack"; import { ADateTag } from "#/core/common/component/adate-tag"; interface BigPlanMilestoneStackProps { milestones: Array<BigPlanMilestone>; + linksEnabled?: boolean; } export function BigPlanMilestoneStack(props: BigPlanMilestoneStackProps) { const sortedMilestones = sortBigPlanMilestones(props.milestones); + const linksEnabled = props.linksEnabled ?? true; return ( <EntityStack> {sortedMilestones.map((milestone) => { + const content = ( + <> + <EntityNameComponent name={milestone.name} /> + <ADateTag label="Date" date={milestone.date} /> + </> + ); + return ( <EntityCard key={`big-plan-milestone-${milestone.ref_id}`} entityId={`big-plan-milestone-${milestone.ref_id}`} > - <EntityLink - to={`/app/workspace/big-plans/${milestone.big_plan_ref_id}/milestones/${milestone.ref_id}`} - > - <EntityNameComponent name={milestone.name} /> - <ADateTag label="Date" date={milestone.date} /> - </EntityLink> + {linksEnabled ? ( + <EntityLink + to={`/app/workspace/big-plans/${milestone.big_plan_ref_id}/milestones/${milestone.ref_id}`} + > + {content} + </EntityLink> + ) : ( + <EntityFakeLink inline>{content}</EntityFakeLink> + )} </EntityCard> ); })} diff --git a/src/core/jupiter/core/big_plans/use_case/load.py b/src/core/jupiter/core/big_plans/use_case/load.py index 07afd399c..c47fb3f04 100644 --- a/src/core/jupiter/core/big_plans/use_case/load.py +++ b/src/core/jupiter/core/big_plans/use_case/load.py @@ -1,47 +1,24 @@ """Use case for loading big plans.""" from jupiter.core.big_plans.root import BigPlan -from jupiter.core.big_plans.stats import BigPlanStats, BigPlanStatsRepository -from jupiter.core.big_plans.sub.milestones.root import BigPlanMilestone -from jupiter.core.common.sub.contacts.root import ContactDomain -from jupiter.core.common.sub.contacts.sub.contact.root import Contact -from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository -from jupiter.core.common.sub.inbox_tasks.collection import ( - InboxTaskCollection, -) -from jupiter.core.common.sub.inbox_tasks.root import ( - InboxTask, - InboxTaskRepository, -) -from jupiter.core.common.sub.notes.root import Note, NoteRepository -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository -from jupiter.core.common.sub.time_events.domain import TimeEventDomain -from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( - TimeEventInDayBlock, -) +from jupiter.core.big_plans.service.load import BigPlanLoadResult, BigPlanLoadService from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.life_plan.sub.aspects.root import Aspect -from jupiter.core.life_plan.sub.chapters.root import Chapter -from jupiter.core.life_plan.sub.goals.root import Goal -from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import ( readonly_use_case, ) from jupiter.framework.use_case_io import ( UseCaseArgsBase, - UseCaseResultBase, use_case_args, - use_case_result, ) +__all__ = ["BigPlanLoadArgs", "BigPlanLoadResult", "BigPlanLoadUseCase"] + @use_case_args class BigPlanLoadArgs(UseCaseArgsBase): @@ -51,23 +28,6 @@ class BigPlanLoadArgs(UseCaseArgsBase): allow_archived: bool | None -@use_case_result -class BigPlanLoadResult(UseCaseResultBase): - """BigPlanLoadResult.""" - - big_plan: BigPlan - aspect: Aspect - chapter: Chapter | None - goal: Goal | None - milestones: list[BigPlanMilestone] - inbox_tasks: list[InboxTask] - tags: list[Tag] - contacts: list[Contact] - note: Note | None - time_event_blocks: list[TimeEventInDayBlock] - stats: BigPlanStats - - @readonly_use_case(WorkspaceFeature.BIG_PLANS) class BigPlanLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[BigPlanLoadArgs, BigPlanLoadResult] @@ -82,92 +42,14 @@ async def _perform_transactional_read( ) -> BigPlanLoadResult: """Execute the command's action.""" allow_archived = args.allow_archived or False - workspace = context.workspace - big_plan = await uow.get_for(BigPlan).load_by_id( args.ref_id, allow_archived=allow_archived ) - aspect = await uow.get_for(Aspect).load_by_id(big_plan.aspect_ref_id) - chapter = ( - await uow.get_for(Chapter).load_by_id(big_plan.chapter_ref_id) - if big_plan.chapter_ref_id - else None - ) - goal = ( - await uow.get_for(Goal).load_by_id(big_plan.goal_ref_id) - if big_plan.goal_ref_id - else None - ) - milestones = await uow.get_for(BigPlanMilestone).find_all_generic( - parent_ref_id=big_plan.ref_id, - allow_archived=False, - ) - inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( - workspace.ref_id, - ) - inbox_tasks = await uow.get( - InboxTaskRepository - ).find_all_for_owner_created_desc( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=allow_archived, - owner=EntityLink.std(NamedEntityTag.BIG_PLAN.value, big_plan.ref_id), - ) - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.BIG_PLAN.value, big_plan.ref_id), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - contact_domain = await uow.get_for(ContactDomain).load_by_parent( + return await BigPlanLoadService().do_it( + uow, workspace.ref_id, - ) - contact_link = await uow.get(ContactLinkRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.BIG_PLAN.value, big_plan.ref_id), - ) - if contact_link is not None: - contacts = await uow.get_for(Contact).find_all_generic( - parent_ref_id=contact_domain.ref_id, - allow_archived=False, - ref_id=contact_link.contacts_ref_ids, - ) - else: - contacts = [] - - note = await uow.get(NoteRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.BIG_PLAN.value, big_plan.ref_id), + big_plan, allow_archived=allow_archived, ) - time_event_domain = await uow.get_for(TimeEventDomain).load_by_parent( - workspace.ref_id - ) - time_event_blocks = await uow.get_for(TimeEventInDayBlock).find_all_generic( - parent_ref_id=time_event_domain.ref_id, - allow_archived=False, - owner=EntityLink.std(NamedEntityTag.BIG_PLAN.value, big_plan.ref_id), - ) - stats = await uow.get(BigPlanStatsRepository).load_by_key_optional( - big_plan.ref_id - ) - if stats is None: - raise Exception("Stats not found") - - return BigPlanLoadResult( - big_plan=big_plan, - aspect=aspect, - chapter=chapter, - goal=goal, - milestones=milestones, - inbox_tasks=inbox_tasks, - tags=tags, - contacts=contacts, - note=note, - time_event_blocks=time_event_blocks, - stats=stats, - ) diff --git a/src/core/jupiter/core/big_plans/use_case/load_public.py b/src/core/jupiter/core/big_plans/use_case/load_public.py new file mode 100644 index 000000000..d5eda6b42 --- /dev/null +++ b/src/core/jupiter/core/big_plans/use_case/load_public.py @@ -0,0 +1,86 @@ +"""Guest readonly use case for loading a published big plan.""" + +from jupiter.core.big_plans.collection import BigPlanCollection +from jupiter.core.big_plans.root import BigPlan +from jupiter.core.big_plans.service.load import BigPlanLoadResult, BigPlanLoadService +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class BigPlanLoadPublicArgs(UseCaseArgsBase): + """BigPlanLoadPublic args.""" + + external_id: PublishExternalId + inbox_task_retrieve_offset: int | None + + +class BigPlanLoadPublicUseCase( + JupiterGuestReadonlyUseCase[BigPlanLoadPublicArgs, BigPlanLoadResult] +): + """Load a published big plan by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: BigPlanLoadPublicArgs, + ) -> BigPlanLoadResult: + """Execute the use case.""" + if ( + args.inbox_task_retrieve_offset is not None + and args.inbox_task_retrieve_offset < 0 + ): + raise InputValidationError("Invalid inbox_task_retrieve_offset") + + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.BIG_PLAN.value: + raise InputValidationError( + "The publish entity does not refer to a big plan." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + big_plan_collection = await uow.get_for(BigPlanCollection).load_by_parent( + publish_domain.workspace.ref_id + ) + big_plan = await uow.get_for(BigPlan).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if big_plan.big_plan_collection.ref_id != big_plan_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace big plan." + ) + + return await BigPlanLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + big_plan, + allow_archived=False, + inbox_task_retrieve_offset=args.inbox_task_retrieve_offset or 0, + paginate_inbox_tasks=True, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index bf1cafda9..988c1821a 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -33,7 +33,7 @@ NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, # done NamedEntityTag.HABIT.value, # done NamedEntityTag.CHORE.value, # done - NamedEntityTag.BIG_PLAN.value, + NamedEntityTag.BIG_PLAN.value, # done NamedEntityTag.DOC.value, # done NamedEntityTag.DIR.value, NamedEntityTag.JOURNAL.value, # done diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx index 59ef60ff1..c7fe347cc 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -41,6 +41,8 @@ function publishedEntityLocation(externalId: string, owner: string): string { return `/app/public/published/habit/${externalId}`; case NamedEntityTag.CHORE: return `/app/public/published/chore/${externalId}`; + case NamedEntityTag.BIG_PLAN: + return `/app/public/published/big-plan/${externalId}`; default: throw new Response(ReasonPhrases.NOT_FOUND, { status: StatusCodes.NOT_FOUND, diff --git a/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx b/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx new file mode 100644 index 000000000..7805102a6 --- /dev/null +++ b/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx @@ -0,0 +1,186 @@ +import type { BigPlanLoadResult, InboxTask } from "@jupiter/webapi-client"; +import { ApiError } from "@jupiter/webapi-client"; +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { useContext, useMemo } from "react"; +import { z } from "zod"; +import { parseParams, parseQuery } from "zodix"; +import { sortInboxTasksNaturally } from "#/core/common/sub/inbox_tasks/root"; +import { InboxTaskStack } from "#/core/common/sub/inbox_tasks/component/stack"; +import { BigPlanPropertiesEditor } from "@jupiter/core/big_plans/component/properties-editor"; +import { BigPlanMilestoneStack } from "@jupiter/core/big_plans/sub/milestones/component/stack"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +const QuerySchema = z.object({ + inboxTasksRetrieveOffset: z + .string() + .transform((s) => parseInt(s, 10)) + .optional(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + const { externalId } = parseParams(params, ParamsSchema); + const query = parseQuery(request, QuerySchema); + const apiClient = await getGuestApiClient(request); + + try { + const result = await apiClient.bigPlans.bigPlanLoadPublic({ + external_id: externalId, + inbox_task_retrieve_offset: query.inboxTasksRetrieveOffset, + }); + + return json({ + bigPlan: result.big_plan, + stats: result.stats, + aspect: result.aspect, + chapter: result.chapter ?? null, + goal: result.goal ?? null, + milestones: result.milestones ?? [], + tags: result.tags ?? [], + contacts: result.contacts ?? [], + note: result.note ?? null, + inboxTasks: result.inbox_tasks as Array<InboxTask>, + inboxTasksTotalCnt: result.inbox_tasks_total_cnt, + inboxTasksPageSize: result.inbox_tasks_page_size, + }); + } catch (error) { + if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw error; + } +} + +export default function PublishedBigPlan() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + + const sortedInboxTasks = useMemo( + () => + sortInboxTasksNaturally(loaderData.inboxTasks, { + dueDateAscending: false, + }), + [loaderData.inboxTasks], + ); + + const bigPlanInfo: BigPlanLoadResult = { + big_plan: loaderData.bigPlan, + aspect: loaderData.aspect, + chapter: loaderData.chapter, + goal: loaderData.goal, + milestones: loaderData.milestones, + inbox_tasks: loaderData.inboxTasks, + inbox_tasks_total_cnt: loaderData.inboxTasksTotalCnt, + inbox_tasks_page_size: loaderData.inboxTasksPageSize, + tags: loaderData.tags, + contacts: loaderData.contacts, + note: loaderData.note, + time_event_blocks: [], + stats: loaderData.stats, + }; + + const allAspects = loaderData.aspect ? [loaderData.aspect] : []; + const allChapters = loaderData.chapter ? [loaderData.chapter] : []; + const allGoals = loaderData.goal ? [loaderData.goal] : []; + + return ( + <LeafPanel + key={`published-big-plan-${loaderData.bigPlan.ref_id}`} + fakeKey={`published-big-plan-${loaderData.bigPlan.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + > + <BigPlanPropertiesEditor + title="Properties" + topLevelInfo={topLevelInfo} + lifePlan={null} + allAspects={allAspects} + allChapters={allChapters} + allGoals={allGoals} + allMilestones={[]} + allTags={loaderData.tags} + tags={loaderData.tags} + allContacts={loaderData.contacts} + contacts={loaderData.contacts} + inputsEnabled={false} + bigPlan={loaderData.bigPlan} + bigPlanInfo={bigPlanInfo} + /> + + <SectionCard title="Note"> + {loaderData.note ? ( + <EntityNoteEditor + initialNote={loaderData.note} + inputsEnabled={false} + /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + + <SectionCard title="Milestones"> + {loaderData.milestones.length > 0 && ( + <BigPlanMilestoneStack + milestones={loaderData.milestones} + linksEnabled={false} + /> + )} + </SectionCard> + + <SectionCard title="Inbox Tasks"> + {sortedInboxTasks.length > 0 && ( + <InboxTaskStack + topLevelInfo={topLevelInfo} + showOptions={{ + showStatus: true, + showDueDate: true, + }} + inboxTasks={sortedInboxTasks} + linksEnabled={false} + withPages={{ + retrieveOffsetParamName: "inboxTasksRetrieveOffset", + totalCnt: loaderData.inboxTasksTotalCnt, + pageSize: loaderData.inboxTasksPageSize, + }} + /> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => + `Could not find published big plan ${params.externalId}!`, + error: (params) => + `There was an error loading published big plan ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/workspace/big-plans/$id.tsx b/src/webui/app/routes/app/workspace/big-plans/$id.tsx index dcb6e1538..1226a0778 100644 --- a/src/webui/app/routes/app/workspace/big-plans/$id.tsx +++ b/src/webui/app/routes/app/workspace/big-plans/$id.tsx @@ -164,6 +164,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("refresh-stats"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -233,6 +245,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { summaryResponse.milestones as Array<MilestoneSummary> | null, allTags: allTags.tags as Array<Tag>, allContacts: allContacts.contacts as Array<Contact>, + publishEntity: result.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -385,6 +398,30 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/app/workspace/big-plans/${id}`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/big-plans/${id}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/big-plans/${id}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/big-plans/${id}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -396,6 +433,10 @@ export async function action({ request, params }: ActionFunctionArgs) { return json(validationErrorToUIErrorInfo(error.body)); } + if (error instanceof ApiError && error.status === StatusCodes.CONFLICT) { + return json(validationErrorToUIErrorInfo(error.body)); + } + throw error; } } @@ -595,6 +636,8 @@ export default function BigPlan() { returnLocation={"/app/workspace/big-plans"} shouldShowALeaflet={shouldShowALeaflet} initialExpansionState={LeafPanelExpansionState.MEDIUM} + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <NestingAwareBlock shouldHide={shouldShowALeaflet}> <GlobalError actionResult={actionData} /> From 6b0efd44d7ff1036e4cce1c567bc5dfd743c9ff6 Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Mon, 8 Jun 2026 12:03:08 +0300 Subject: [PATCH 10/21] Add support for smart list sharing --- ...t_list_item_load_public_from_smart_list.py | 216 +++++++++++++++++ .../api/smart_lists/smart_list_load_public.py | 212 ++++++++++++++++ .../jupiter_webapi_client/models/__init__.py | 6 + ...t_item_load_public_from_smart_list_args.py | 70 ++++++ .../models/smart_list_load_public_args.py | 84 +++++++ .../models/smart_list_load_result.py | 74 ++++++ ..._result_smart_list_item_contacts_type_0.py | 68 ++++++ gen/ts/webapi-client/gen/index.ts | 2 + ...martListItemLoadPublicFromSmartListArgs.ts | 14 ++ .../gen/models/SmartListLoadPublicArgs.ts | 13 + .../gen/models/SmartListLoadResult.ts | 4 + .../gen/services/SmartListsService.ts | 58 +++++ itests/webui/entities/smart_list.test.py | 74 +++++- .../common/sub/publish/sub/entity/root.py | 2 +- .../core/infra/component/error-boundary.tsx | 59 ++++- .../infra/component/layout/leaf-panel.tsx | 16 +- src/core/jupiter/core/smart_lists/root.py | 4 + .../jupiter/core/smart_lists/service/load.py | 186 ++++++++++++++ .../use_case/load_public_from_smart_list.py | 94 +++++++ .../jupiter/core/smart_lists/use_case/load.py | 111 +-------- .../core/smart_lists/use_case/load_public.py | 84 +++++++ .../app/rendering/published-loader.server.ts | 42 ++++ .../app/public/published/$externalId.tsx | 22 +- .../public/published/big-plan/$externalId.tsx | 20 +- .../public/published/chore/$externalId.tsx | 20 +- .../app/public/published/doc/$externalId.tsx | 19 +- .../public/published/habit/$externalId.tsx | 33 +-- .../public/published/journal/$externalId.tsx | 18 +- .../published/metric-entry/$externalId.tsx | 18 +- .../public/published/person/$externalId.tsx | 18 +- .../schedule-event-full-days/$externalId.tsx | 18 +- .../schedule-event-in-day/$externalId.tsx | 18 +- .../published/smart-list/$externalId.tsx | 229 ++++++++++++++++++ .../smart-list/$externalId/$itemId.tsx | 97 ++++++++ .../item}/$externalId.tsx | 18 +- .../published/time-plan/$externalId.tsx | 19 +- .../published/todo-task/$externalId.tsx | 18 +- .../public/published/vacation/$externalId.tsx | 18 +- .../routes/app/workspace/big-plans/$id.tsx | 4 + .../core/publish/$publishEntityId.tsx | 1 - .../routes/app/workspace/smart-lists/$id.tsx | 99 +++++--- .../app/workspace/smart-lists/$id/$itemId.tsx | 4 + 42 files changed, 1870 insertions(+), 334 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_item_load_public_from_smart_list.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_public_from_smart_list_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_public_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_result_smart_list_item_contacts_type_0.py create mode 100644 gen/ts/webapi-client/gen/models/SmartListItemLoadPublicFromSmartListArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/SmartListLoadPublicArgs.ts create mode 100644 src/core/jupiter/core/smart_lists/service/load.py create mode 100644 src/core/jupiter/core/smart_lists/sub/item/use_case/load_public_from_smart_list.py create mode 100644 src/core/jupiter/core/smart_lists/use_case/load_public.py create mode 100644 src/webui/app/rendering/published-loader.server.ts create mode 100644 src/webui/app/routes/app/public/published/smart-list/$externalId.tsx create mode 100644 src/webui/app/routes/app/public/published/smart-list/$externalId/$itemId.tsx rename src/webui/app/routes/app/public/published/{smart-list-item => smart-list/item}/$externalId.tsx (86%) diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_item_load_public_from_smart_list.py b/gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_item_load_public_from_smart_list.py new file mode 100644 index 000000000..f20d0fa0b --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_item_load_public_from_smart_list.py @@ -0,0 +1,216 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.smart_list_item_load_public_from_smart_list_args import SmartListItemLoadPublicFromSmartListArgs +from ...models.smart_list_item_load_result import SmartListItemLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: SmartListItemLoadPublicFromSmartListArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/smart-list-item-load-public-from-smart-list", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | SmartListItemLoadResult | None: + if response.status_code == 200: + response_200 = SmartListItemLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | SmartListItemLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: SmartListItemLoadPublicFromSmartListArgs | Unset = UNSET, +) -> Response[ErrorResponse | SmartListItemLoadResult]: + """Load a smart list item through a published smart list. + + Args: + body (SmartListItemLoadPublicFromSmartListArgs | Unset): + SmartListItemLoadPublicFromSmartList args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | SmartListItemLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: SmartListItemLoadPublicFromSmartListArgs | Unset = UNSET, +) -> ErrorResponse | SmartListItemLoadResult | None: + """Load a smart list item through a published smart list. + + Args: + body (SmartListItemLoadPublicFromSmartListArgs | Unset): + SmartListItemLoadPublicFromSmartList args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | SmartListItemLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: SmartListItemLoadPublicFromSmartListArgs | Unset = UNSET, +) -> Response[ErrorResponse | SmartListItemLoadResult]: + """Load a smart list item through a published smart list. + + Args: + body (SmartListItemLoadPublicFromSmartListArgs | Unset): + SmartListItemLoadPublicFromSmartList args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | SmartListItemLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: SmartListItemLoadPublicFromSmartListArgs | Unset = UNSET, +) -> ErrorResponse | SmartListItemLoadResult | None: + """Load a smart list item through a published smart list. + + Args: + body (SmartListItemLoadPublicFromSmartListArgs | Unset): + SmartListItemLoadPublicFromSmartList args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | SmartListItemLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_load_public.py new file mode 100644 index 000000000..2f1c6bd4f --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/smart_lists/smart_list_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.smart_list_load_public_args import SmartListLoadPublicArgs +from ...models.smart_list_load_result import SmartListLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: SmartListLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/smart-list-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | SmartListLoadResult | None: + if response.status_code == 200: + response_200 = SmartListLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | SmartListLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: SmartListLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | SmartListLoadResult]: + """Load a published smart list by publish external id. + + Args: + body (SmartListLoadPublicArgs | Unset): SmartListLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | SmartListLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: SmartListLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | SmartListLoadResult | None: + """Load a published smart list by publish external id. + + Args: + body (SmartListLoadPublicArgs | Unset): SmartListLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | SmartListLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: SmartListLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | SmartListLoadResult]: + """Load a published smart list by publish external id. + + Args: + body (SmartListLoadPublicArgs | Unset): SmartListLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | SmartListLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: SmartListLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | SmartListLoadResult | None: + """Load a published smart list by publish external id. + + Args: + body (SmartListLoadPublicArgs | Unset): SmartListLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | SmartListLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index a47cf1ca2..2e97b04bd 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -785,6 +785,7 @@ from .smart_list_item_create_result import SmartListItemCreateResult from .smart_list_item_load_args import SmartListItemLoadArgs from .smart_list_item_load_public_args import SmartListItemLoadPublicArgs +from .smart_list_item_load_public_from_smart_list_args import SmartListItemLoadPublicFromSmartListArgs from .smart_list_item_load_result import SmartListItemLoadResult from .smart_list_item_remove_args import SmartListItemRemoveArgs from .smart_list_item_update_args import SmartListItemUpdateArgs @@ -792,7 +793,9 @@ from .smart_list_item_update_args_name import SmartListItemUpdateArgsName from .smart_list_item_update_args_url import SmartListItemUpdateArgsUrl from .smart_list_load_args import SmartListLoadArgs +from .smart_list_load_public_args import SmartListLoadPublicArgs from .smart_list_load_result import SmartListLoadResult +from .smart_list_load_result_smart_list_item_contacts_type_0 import SmartListLoadResultSmartListItemContactsType0 from .smart_list_load_result_smart_list_item_generic_tags_type_0 import SmartListLoadResultSmartListItemGenericTagsType0 from .smart_list_remove_args import SmartListRemoveArgs from .smart_list_summary import SmartListSummary @@ -1794,6 +1797,7 @@ "SmartListItemCreateResult", "SmartListItemLoadArgs", "SmartListItemLoadPublicArgs", + "SmartListItemLoadPublicFromSmartListArgs", "SmartListItemLoadResult", "SmartListItemRemoveArgs", "SmartListItemUpdateArgs", @@ -1801,7 +1805,9 @@ "SmartListItemUpdateArgsName", "SmartListItemUpdateArgsUrl", "SmartListLoadArgs", + "SmartListLoadPublicArgs", "SmartListLoadResult", + "SmartListLoadResultSmartListItemContactsType0", "SmartListLoadResultSmartListItemGenericTagsType0", "SmartListRemoveArgs", "SmartListSummary", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_public_from_smart_list_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_public_from_smart_list_args.py new file mode 100644 index 000000000..50e947b54 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_item_load_public_from_smart_list_args.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="SmartListItemLoadPublicFromSmartListArgs") + + +@_attrs_define +class SmartListItemLoadPublicFromSmartListArgs: + """SmartListItemLoadPublicFromSmartList args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + ref_id (str): A generic entity id. + """ + + external_id: str + ref_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + ref_id = self.ref_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + "ref_id": ref_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + ref_id = d.pop("ref_id") + + smart_list_item_load_public_from_smart_list_args = cls( + external_id=external_id, + ref_id=ref_id, + ) + + smart_list_item_load_public_from_smart_list_args.additional_properties = d + return smart_list_item_load_public_from_smart_list_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_public_args.py new file mode 100644 index 000000000..403e44048 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_public_args.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="SmartListLoadPublicArgs") + + +@_attrs_define +class SmartListLoadPublicArgs: + """SmartListLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + include_item_tags_and_notes (bool | None | Unset): + """ + + external_id: str + include_item_tags_and_notes: bool | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + include_item_tags_and_notes: bool | None | Unset + if isinstance(self.include_item_tags_and_notes, Unset): + include_item_tags_and_notes = UNSET + else: + include_item_tags_and_notes = self.include_item_tags_and_notes + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + if include_item_tags_and_notes is not UNSET: + field_dict["include_item_tags_and_notes"] = include_item_tags_and_notes + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + def _parse_include_item_tags_and_notes(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + include_item_tags_and_notes = _parse_include_item_tags_and_notes(d.pop("include_item_tags_and_notes", UNSET)) + + smart_list_load_public_args = cls( + external_id=external_id, + include_item_tags_and_notes=include_item_tags_and_notes, + ) + + smart_list_load_public_args.additional_properties = d + return smart_list_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_result.py index 84d80f20c..8852a458d 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_result.py @@ -10,8 +10,12 @@ if TYPE_CHECKING: from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.smart_list import SmartList from ..models.smart_list_item import SmartListItem + from ..models.smart_list_load_result_smart_list_item_contacts_type_0 import ( + SmartListLoadResultSmartListItemContactsType0, + ) from ..models.smart_list_load_result_smart_list_item_generic_tags_type_0 import ( SmartListLoadResultSmartListItemGenericTagsType0, ) @@ -31,7 +35,9 @@ class SmartListLoadResult: smart_list_items (list[SmartListItem]): note (None | Note | Unset): smart_list_item_generic_tags (None | SmartListLoadResultSmartListItemGenericTagsType0 | Unset): + smart_list_item_contacts (None | SmartListLoadResultSmartListItemContactsType0 | Unset): smart_list_item_notes (list[Note] | None | Unset): + publish_entity (None | PublishEntity | Unset): """ smart_list: SmartList @@ -39,11 +45,17 @@ class SmartListLoadResult: smart_list_items: list[SmartListItem] note: None | Note | Unset = UNSET smart_list_item_generic_tags: None | SmartListLoadResultSmartListItemGenericTagsType0 | Unset = UNSET + smart_list_item_contacts: None | SmartListLoadResultSmartListItemContactsType0 | Unset = UNSET smart_list_item_notes: list[Note] | None | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.note import Note + from ..models.publish_entity import PublishEntity + from ..models.smart_list_load_result_smart_list_item_contacts_type_0 import ( + SmartListLoadResultSmartListItemContactsType0, + ) from ..models.smart_list_load_result_smart_list_item_generic_tags_type_0 import ( SmartListLoadResultSmartListItemGenericTagsType0, ) @@ -76,6 +88,14 @@ def to_dict(self) -> dict[str, Any]: else: smart_list_item_generic_tags = self.smart_list_item_generic_tags + smart_list_item_contacts: dict[str, Any] | None | Unset + if isinstance(self.smart_list_item_contacts, Unset): + smart_list_item_contacts = UNSET + elif isinstance(self.smart_list_item_contacts, SmartListLoadResultSmartListItemContactsType0): + smart_list_item_contacts = self.smart_list_item_contacts.to_dict() + else: + smart_list_item_contacts = self.smart_list_item_contacts + smart_list_item_notes: list[dict[str, Any]] | None | Unset if isinstance(self.smart_list_item_notes, Unset): smart_list_item_notes = UNSET @@ -88,6 +108,14 @@ def to_dict(self) -> dict[str, Any]: else: smart_list_item_notes = self.smart_list_item_notes + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -101,16 +129,24 @@ def to_dict(self) -> dict[str, Any]: field_dict["note"] = note if smart_list_item_generic_tags is not UNSET: field_dict["smart_list_item_generic_tags"] = smart_list_item_generic_tags + if smart_list_item_contacts is not UNSET: + field_dict["smart_list_item_contacts"] = smart_list_item_contacts if smart_list_item_notes is not UNSET: field_dict["smart_list_item_notes"] = smart_list_item_notes + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.smart_list import SmartList from ..models.smart_list_item import SmartListItem + from ..models.smart_list_load_result_smart_list_item_contacts_type_0 import ( + SmartListLoadResultSmartListItemContactsType0, + ) from ..models.smart_list_load_result_smart_list_item_generic_tags_type_0 import ( SmartListLoadResultSmartListItemGenericTagsType0, ) @@ -169,6 +205,25 @@ def _parse_smart_list_item_generic_tags( smart_list_item_generic_tags = _parse_smart_list_item_generic_tags(d.pop("smart_list_item_generic_tags", UNSET)) + def _parse_smart_list_item_contacts( + data: object, + ) -> None | SmartListLoadResultSmartListItemContactsType0 | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + smart_list_item_contacts_type_0 = SmartListLoadResultSmartListItemContactsType0.from_dict(data) + + return smart_list_item_contacts_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | SmartListLoadResultSmartListItemContactsType0 | Unset, data) + + smart_list_item_contacts = _parse_smart_list_item_contacts(d.pop("smart_list_item_contacts", UNSET)) + def _parse_smart_list_item_notes(data: object) -> list[Note] | None | Unset: if data is None: return data @@ -191,13 +246,32 @@ def _parse_smart_list_item_notes(data: object) -> list[Note] | None | Unset: smart_list_item_notes = _parse_smart_list_item_notes(d.pop("smart_list_item_notes", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + smart_list_load_result = cls( smart_list=smart_list, tags=tags, smart_list_items=smart_list_items, note=note, smart_list_item_generic_tags=smart_list_item_generic_tags, + smart_list_item_contacts=smart_list_item_contacts, smart_list_item_notes=smart_list_item_notes, + publish_entity=publish_entity, ) smart_list_load_result.additional_properties = d diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_result_smart_list_item_contacts_type_0.py b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_result_smart_list_item_contacts_type_0.py new file mode 100644 index 000000000..b5f5d5f9e --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/smart_list_load_result_smart_list_item_contacts_type_0.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.contact import Contact + + +T = TypeVar("T", bound="SmartListLoadResultSmartListItemContactsType0") + + +@_attrs_define +class SmartListLoadResultSmartListItemContactsType0: + """ """ + + additional_properties: dict[str, list[Contact]] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = [] + for additional_property_item_data in prop: + additional_property_item = additional_property_item_data.to_dict() + field_dict[prop_name].append(additional_property_item) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.contact import Contact + + d = dict(src_dict) + smart_list_load_result_smart_list_item_contacts_type_0 = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = [] + _additional_property = prop_dict + for additional_property_item_data in _additional_property: + additional_property_item = Contact.from_dict(additional_property_item_data) + + additional_property.append(additional_property_item) + + additional_properties[prop_name] = additional_property + + smart_list_load_result_smart_list_item_contacts_type_0.additional_properties = additional_properties + return smart_list_load_result_smart_list_item_contacts_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> list[Contact]: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: list[Contact]) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index 7188f8c53..67e461209 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -652,11 +652,13 @@ export type { SmartListItemCreateArgs } from './models/SmartListItemCreateArgs'; export type { SmartListItemCreateResult } from './models/SmartListItemCreateResult'; export type { SmartListItemLoadArgs } from './models/SmartListItemLoadArgs'; export type { SmartListItemLoadPublicArgs } from './models/SmartListItemLoadPublicArgs'; +export type { SmartListItemLoadPublicFromSmartListArgs } from './models/SmartListItemLoadPublicFromSmartListArgs'; export type { SmartListItemLoadResult } from './models/SmartListItemLoadResult'; export type { SmartListItemName } from './models/SmartListItemName'; export type { SmartListItemRemoveArgs } from './models/SmartListItemRemoveArgs'; export type { SmartListItemUpdateArgs } from './models/SmartListItemUpdateArgs'; export type { SmartListLoadArgs } from './models/SmartListLoadArgs'; +export type { SmartListLoadPublicArgs } from './models/SmartListLoadPublicArgs'; export type { SmartListLoadResult } from './models/SmartListLoadResult'; export type { SmartListName } from './models/SmartListName'; export type { SmartListRemoveArgs } from './models/SmartListRemoveArgs'; diff --git a/gen/ts/webapi-client/gen/models/SmartListItemLoadPublicFromSmartListArgs.ts b/gen/ts/webapi-client/gen/models/SmartListItemLoadPublicFromSmartListArgs.ts new file mode 100644 index 000000000..3bff8ebe4 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/SmartListItemLoadPublicFromSmartListArgs.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { PublishExternalId } from './PublishExternalId'; +/** + * SmartListItemLoadPublicFromSmartList args. + */ +export type SmartListItemLoadPublicFromSmartListArgs = { + external_id: PublishExternalId; + ref_id: EntityId; +}; + diff --git a/gen/ts/webapi-client/gen/models/SmartListLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/SmartListLoadPublicArgs.ts new file mode 100644 index 000000000..ab1d81f49 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/SmartListLoadPublicArgs.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * SmartListLoadPublic args. + */ +export type SmartListLoadPublicArgs = { + external_id: PublishExternalId; + include_item_tags_and_notes?: (boolean | null); +}; + diff --git a/gen/ts/webapi-client/gen/models/SmartListLoadResult.ts b/gen/ts/webapi-client/gen/models/SmartListLoadResult.ts index 56ac861b9..6ebd97e4e 100644 --- a/gen/ts/webapi-client/gen/models/SmartListLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/SmartListLoadResult.ts @@ -2,7 +2,9 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { Contact } from './Contact'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { SmartList } from './SmartList'; import type { SmartListItem } from './SmartListItem'; import type { Tag } from './Tag'; @@ -15,6 +17,8 @@ export type SmartListLoadResult = { note?: (Note | null); smart_list_items: Array<SmartListItem>; smart_list_item_generic_tags?: (Record<string, Array<Tag>> | null); + smart_list_item_contacts?: (Record<string, Array<Contact>> | null); smart_list_item_notes?: (Array<Note> | null); + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/services/SmartListsService.ts b/gen/ts/webapi-client/gen/services/SmartListsService.ts index e05d88101..d2bbb714b 100644 --- a/gen/ts/webapi-client/gen/services/SmartListsService.ts +++ b/gen/ts/webapi-client/gen/services/SmartListsService.ts @@ -12,10 +12,12 @@ import type { SmartListItemCreateArgs } from '../models/SmartListItemCreateArgs' import type { SmartListItemCreateResult } from '../models/SmartListItemCreateResult'; import type { SmartListItemLoadArgs } from '../models/SmartListItemLoadArgs'; import type { SmartListItemLoadPublicArgs } from '../models/SmartListItemLoadPublicArgs'; +import type { SmartListItemLoadPublicFromSmartListArgs } from '../models/SmartListItemLoadPublicFromSmartListArgs'; import type { SmartListItemLoadResult } from '../models/SmartListItemLoadResult'; import type { SmartListItemRemoveArgs } from '../models/SmartListItemRemoveArgs'; import type { SmartListItemUpdateArgs } from '../models/SmartListItemUpdateArgs'; import type { SmartListLoadArgs } from '../models/SmartListLoadArgs'; +import type { SmartListLoadPublicArgs } from '../models/SmartListLoadPublicArgs'; import type { SmartListLoadResult } from '../models/SmartListLoadResult'; import type { SmartListRemoveArgs } from '../models/SmartListRemoveArgs'; import type { SmartListUpdateArgs } from '../models/SmartListUpdateArgs'; @@ -135,6 +137,34 @@ export class SmartListsService { }, }); } + /** + * Load a smart list item through a published smart list. + * @param requestBody The input data + * @returns SmartListItemLoadResult Successful response + * @throws ApiError + */ + public smartListItemLoadPublicFromSmartList( + requestBody?: SmartListItemLoadPublicFromSmartListArgs, + ): CancelablePromise<SmartListItemLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/smart-list-item-load-public-from-smart-list', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * The command for removing a smart list item. * @param requestBody The input data @@ -303,6 +333,34 @@ export class SmartListsService { }, }); } + /** + * Load a published smart list by publish external id. + * @param requestBody The input data + * @returns SmartListLoadResult Successful response + * @throws ApiError + */ + public smartListLoadPublic( + requestBody?: SmartListLoadPublicArgs, + ): CancelablePromise<SmartListLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/smart-list-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * The command for removing a smart list. * @param requestBody The input data diff --git a/itests/webui/entities/smart_list.test.py b/itests/webui/entities/smart_list.test.py index c3020249e..45cc14bf6 100644 --- a/itests/webui/entities/smart_list.test.py +++ b/itests/webui/entities/smart_list.test.py @@ -29,7 +29,11 @@ ) from playwright.sync_api import Page, expect -from itests.helpers import get_parsed_from_response, open_leaf_publish_panel +from itests.helpers import ( + get_parsed_from_response, + open_branch_publish_panel, + open_leaf_publish_panel, +) @pytest.fixture(autouse=True, scope="module") @@ -135,6 +139,72 @@ def test_webui_smart_list_view_one_items( ) +def test_webui_smart_list_publish_and_view_public( + page: Page, create_smart_list, create_smart_list_item +) -> None: + smart_list = create_smart_list("Published Smart List") + item = create_smart_list_item("Published Smart List Item", smart_list.ref_id) + page.goto(f"/app/workspace/smart-lists/{smart_list.ref_id}") + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "SmartList-publish") + page.locator("button[id='SmartList-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/smart-lists/{smart_list.ref_id}")) + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "SmartList-publish") + expect(page.locator("#SmartList-publish")).to_contain_text("draft") + + page.locator("button[id='SmartList-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/smart-lists/{smart_list.ref_id}")) + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "SmartList-publish") + expect(page.locator("#SmartList-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/smart-list/")) + page.wait_for_selector("#branch-panel") + + expect(page.locator(f"#smart-list-item-{item.ref_id}")).to_contain_text( + "Published Smart List Item" + ) + + +def test_webui_smart_list_item_view_public( + page: Page, create_smart_list, create_smart_list_item +) -> None: + smart_list = create_smart_list("Published Smart List For Item") + item = create_smart_list_item("Public Item Detail", smart_list.ref_id) + page.goto(f"/app/workspace/smart-lists/{smart_list.ref_id}") + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "SmartList-publish") + page.locator("button[id='SmartList-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/smart-lists/{smart_list.ref_id}")) + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "SmartList-publish") + page.locator("button[id='SmartList-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/smart-lists/{smart_list.ref_id}")) + page.wait_for_selector("#branch-panel") + + public_url = page.locator('input[name="publicUrl"]').input_value() + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/smart-list/")) + + page.locator(f"#smart-list-item-{item.ref_id}").click() + page.wait_for_url( + re.compile(rf"/app/public/published/smart-list/[^/]+/{item.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + expect(page.locator('input[name="name"]')).to_have_value("Public Item Detail") + + def test_webui_smart_list_item_publish_and_view_public( page: Page, create_smart_list, create_smart_list_item ) -> None: @@ -166,7 +236,7 @@ def test_webui_smart_list_item_publish_and_view_public( assert "/app/public/published/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/smart-list-item/")) + page.wait_for_url(re.compile(r"/app/public/published/smart-list/item/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value( diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index 988c1821a..341f2d0c3 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -38,7 +38,7 @@ NamedEntityTag.DIR.value, NamedEntityTag.JOURNAL.value, # done NamedEntityTag.VACATION.value, # done - NamedEntityTag.SMART_LIST.value, + NamedEntityTag.SMART_LIST.value, # done NamedEntityTag.SMART_LIST_ITEM.value, # done NamedEntityTag.METRIC.value, NamedEntityTag.METRIC_ENTRY.value, # done diff --git a/src/core/jupiter/core/infra/component/error-boundary.tsx b/src/core/jupiter/core/infra/component/error-boundary.tsx index 72ac396cb..be6efcbae 100644 --- a/src/core/jupiter/core/infra/component/error-boundary.tsx +++ b/src/core/jupiter/core/infra/component/error-boundary.tsx @@ -91,7 +91,10 @@ export function makeLeafErrorBoundary<K extends z.ZodRawShape>( const error = useRouteError(); const globalProperties = useContext(GlobalPropertiesContext); const paramsRaw = useParams(); - const params = paramsParser.parse(paramsRaw); + const parsedParams = paramsParser.safeParse(paramsRaw); + const params = parsedParams.success + ? parsedParams.data + : (paramsRaw as z.infer<typeof paramsParser>); const [searchParams] = useSearchParams(); const resolvedReturnLocation = typeof returnLocation === "function" @@ -100,10 +103,6 @@ export function makeLeafErrorBoundary<K extends z.ZodRawShape>( if (isRouteErrorResponse(error)) { if (error.status === StatusCodes.NOT_FOUND) { - const resolvedReturnLocation = - typeof returnLocation === "function" - ? returnLocation(params, searchParams) - : returnLocation; return ( <LeafPanel key="error" @@ -120,6 +119,29 @@ export function makeLeafErrorBoundary<K extends z.ZodRawShape>( </LeafPanel> ); } + + return ( + <LeafPanel + key="error" + fakeKey="error" + inputsEnabled={true} + returnLocation={resolvedReturnLocation} + > + <Alert severity="error"> + <AlertTitle>Danger</AlertTitle> + {labelsFor.error + ? labelsFor.error(params, searchParams) + : "Error retrieving entity!"} + + {isDevelopment(globalProperties.env) && ( + <Box> + <pre>{error.statusText}</pre> + <pre>{JSON.stringify(error.data, null, 2)}</pre> + </Box> + )} + </Alert> + </LeafPanel> + ); } if (error instanceof Error) { @@ -188,7 +210,10 @@ export function makeBranchErrorBoundary<K extends z.ZodRawShape>( const error = useRouteError(); const globalProperties = useContext(GlobalPropertiesContext); const paramsRaw = useParams(); - const params = paramsParser.parse(paramsRaw); + const parsedParams = paramsParser.safeParse(paramsRaw); + const params = parsedParams.success + ? parsedParams.data + : (paramsRaw as z.infer<typeof paramsParser>); const [searchParams] = useSearchParams(); const resolvedReturnLocation = typeof returnLocation === "function" @@ -212,6 +237,28 @@ export function makeBranchErrorBoundary<K extends z.ZodRawShape>( </BranchPanel> ); } + + return ( + <BranchPanel + key="error" + inputsEnabled={true} + returnLocation={resolvedReturnLocation} + > + <Alert severity="error"> + <AlertTitle>Danger</AlertTitle> + {labelsFor.error + ? labelsFor.error(params, searchParams) + : "Error retrieving entity!"} + + {isDevelopment(globalProperties.env) && ( + <Box> + <pre>{error.statusText}</pre> + <pre>{JSON.stringify(error.data, null, 2)}</pre> + </Box> + )} + </Alert> + </BranchPanel> + ); } if (error instanceof Error) { diff --git a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx index 42564ca9a..6c67029b0 100644 --- a/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx +++ b/src/core/jupiter/core/infra/component/layout/leaf-panel.tsx @@ -89,11 +89,9 @@ export function LeafPanel(props: PropsWithChildren<LeafPanelProps>) { const [expansionState, setExpansionState] = useState< LeafPanelExpansionState | "shrunk" | "exit" >( - props.shouldShowALeaflet - ? LeafPanelExpansionState.LARGE - : props.isLeaflet - ? LeafPanelExpansionState.SMALL - : (props.initialExpansionState ?? LeafPanelExpansionState.SMALL), + props.isLeaflet + ? LeafPanelExpansionState.SMALL + : (props.initialExpansionState ?? LeafPanelExpansionState.SMALL), ); const [previousExpansionState, setPreviousExpansionState] = useState<LeafPanelExpansionState | null>(null); @@ -227,9 +225,11 @@ export function LeafPanel(props: PropsWithChildren<LeafPanelProps>) { } if (props.shouldShowALeaflet) { - // This check here is mostly to prevent the expansion state from being set to LARGE - // when double rendering in React dev mode. - if (expansionState !== LeafPanelExpansionState.LARGE) { + // Grow smaller panels to make room for the leaflet, but keep FULL as-is. + if ( + expansionState !== LeafPanelExpansionState.LARGE && + expansionState !== LeafPanelExpansionState.FULL + ) { setPreviousLeafletExpansionState(expansionState); setExpansionState(LeafPanelExpansionState.LARGE); } diff --git a/src/core/jupiter/core/smart_lists/root.py b/src/core/jupiter/core/smart_lists/root.py index 5b818bae1..de0bb7f9d 100644 --- a/src/core/jupiter/core/smart_lists/root.py +++ b/src/core/jupiter/core/smart_lists/root.py @@ -2,6 +2,7 @@ from jupiter.core.common.entity_icon import EntityIcon from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.core.smart_lists.name import SmartListName @@ -37,6 +38,9 @@ class SmartList(BranchEntity): ) note = OwnsAtMostOne(Note, owner=IsEntityLinkStd(NamedEntityTag.SMART_LIST.value)) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.SMART_LIST.value) + ) @staticmethod @create_entity_action diff --git a/src/core/jupiter/core/smart_lists/service/load.py b/src/core/jupiter/core/smart_lists/service/load.py new file mode 100644 index 000000000..afeeb3682 --- /dev/null +++ b/src/core/jupiter/core/smart_lists/service/load.py @@ -0,0 +1,186 @@ +"""Shared service for loading a smart list and its dependent entities.""" + +from typing import cast + +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.notes.collection import NoteCollection +from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.root import TagDomain +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.smart_lists.root import SmartList +from jupiter.core.smart_lists.sub.item.root import SmartListItem +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result + + +@use_case_result +class SmartListLoadResult(UseCaseResultBase): + """SmartListLoadResult.""" + + smart_list: SmartList + tags: list[Tag] + note: Note | None + smart_list_items: list[SmartListItem] + smart_list_item_generic_tags: dict[EntityId, list[Tag]] | None + smart_list_item_contacts: dict[EntityId, list[Contact]] | None + smart_list_item_notes: list[Note] | None + publish_entity: PublishEntity | None + + +class SmartListLoadService: + """Shared service for loading a smart list and its dependent entities.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + smart_list: SmartList, + *, + allow_archived: bool = False, + allow_archived_items: bool = False, + allow_archived_tags: bool = False, + include_item_tags_and_notes: bool = False, + include_publish_entity: bool = True, + ) -> SmartListLoadResult: + """Load a smart list and its dependent entities.""" + smart_list = await uow.get_for(SmartList).load_by_id( + smart_list.ref_id, + allow_archived=allow_archived, + ) + owner = EntityLink.std(NamedEntityTag.SMART_LIST.value, smart_list.ref_id) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=owner, + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=allow_archived_tags, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + smart_list_items = await uow.get_for(SmartListItem).find_all_generic( + parent_ref_id=smart_list.ref_id, allow_archived=allow_archived_items + ) + + note = await uow.get(NoteRepository).load_optional_for_owner( + owner, + allow_archived=allow_archived, + ) + + smart_list_item_notes: list[Note] | None = None + smart_list_item_generic_tags: dict[EntityId, list[Tag]] | None = None + smart_list_item_contacts: dict[EntityId, list[Contact]] | None = None + + if include_item_tags_and_notes and len(smart_list_items) > 0: + note_collection = await uow.get_for(NoteCollection).load_by_parent( + workspace_ref_id, + ) + smart_list_item_notes = await uow.get( + NoteRepository + ).find_all_for_note_collection( + note_collection_ref_id=note_collection.ref_id, + allow_archived=allow_archived, + filter_owners=[ + EntityLink.std(NamedEntityTag.SMART_LIST_ITEM.value, rid) + for rid in [item.ref_id for item in smart_list_items] + ], + ) + + tags_domain = await uow.get_for(TagDomain).load_by_parent( + workspace_ref_id, + ) + item_tag_links = await uow.get(TagLinkRepository).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=allow_archived_tags, + owner=[ + EntityLink.std(NamedEntityTag.SMART_LIST_ITEM.value, item.ref_id) + for item in smart_list_items + ], + ) + all_item_tag_ref_ids: list[EntityId] = [] + for tl in item_tag_links: + all_item_tag_ref_ids.extend(tl.ref_ids) + if all_item_tag_ref_ids: + all_item_tags = await uow.get_for(Tag).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=allow_archived_tags, + ref_id=list(set(all_item_tag_ref_ids)), + ) + all_item_tags_by_ref_id = {t.ref_id: t for t in all_item_tags} + else: + all_item_tags_by_ref_id = {} + + smart_list_item_generic_tags = { + cast(EntityId, tl.owner.ref_id): [ + all_item_tags_by_ref_id[rid] + for rid in tl.ref_ids + if rid in all_item_tags_by_ref_id + ] + for tl in item_tag_links + } + + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace_ref_id, + ) + item_contact_links = await uow.get(ContactLinkRepository).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + owner=[ + EntityLink.std(NamedEntityTag.SMART_LIST_ITEM.value, item.ref_id) + for item in smart_list_items + ], + ) + all_item_contact_ref_ids: list[EntityId] = [] + for cl in item_contact_links: + all_item_contact_ref_ids.extend(cl.contacts_ref_ids) + if all_item_contact_ref_ids: + all_item_contacts = await uow.get_for(Contact).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + ref_id=list(set(all_item_contact_ref_ids)), + ) + all_item_contacts_by_ref_id = {c.ref_id: c for c in all_item_contacts} + else: + all_item_contacts_by_ref_id = {} + + smart_list_item_contacts = { + cast(EntityId, cl.owner.ref_id): [ + all_item_contacts_by_ref_id[rid] + for rid in cl.contacts_ref_ids + if rid in all_item_contacts_by_ref_id + ] + for cl in item_contact_links + } + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + owner, + allow_archived=allow_archived, + ) + + return SmartListLoadResult( + smart_list=smart_list, + tags=tags, + note=note, + smart_list_items=smart_list_items, + smart_list_item_generic_tags=smart_list_item_generic_tags, + smart_list_item_contacts=smart_list_item_contacts, + smart_list_item_notes=smart_list_item_notes, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/smart_lists/sub/item/use_case/load_public_from_smart_list.py b/src/core/jupiter/core/smart_lists/sub/item/use_case/load_public_from_smart_list.py new file mode 100644 index 000000000..0d76043b6 --- /dev/null +++ b/src/core/jupiter/core/smart_lists/sub/item/use_case/load_public_from_smart_list.py @@ -0,0 +1,94 @@ +"""Guest readonly use case for loading a smart list item via a published list.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.smart_lists.collection import SmartListCollection +from jupiter.core.smart_lists.root import SmartList +from jupiter.core.smart_lists.sub.item.root import SmartListItem +from jupiter.core.smart_lists.sub.item.service.load import ( + SmartListItemLoadResult, + SmartListItemLoadService, +) +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class SmartListItemLoadPublicFromSmartListArgs(UseCaseArgsBase): + """SmartListItemLoadPublicFromSmartList args.""" + + external_id: PublishExternalId + ref_id: EntityId + + +class SmartListItemLoadPublicFromSmartListUseCase( + JupiterGuestReadonlyUseCase[ + SmartListItemLoadPublicFromSmartListArgs, SmartListItemLoadResult + ] +): + """Load a smart list item through a published smart list.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: SmartListItemLoadPublicFromSmartListArgs, + ) -> SmartListItemLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.SMART_LIST.value: + raise InputValidationError( + "The publish entity does not refer to a smart list." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + smart_list_collection = await uow.get_for( + SmartListCollection + ).load_by_parent(publish_domain.workspace.ref_id) + smart_list = await uow.get_for(SmartList).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if smart_list.parent_ref_id != smart_list_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace smart list." + ) + + item = await uow.get_for(SmartListItem).load_by_id( + args.ref_id, + allow_archived=False, + ) + if item.smart_list.ref_id != smart_list.ref_id: + raise InputValidationError( + "The smart list item does not belong to the published smart list." + ) + + return await SmartListItemLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + item, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/smart_lists/use_case/load.py b/src/core/jupiter/core/smart_lists/use_case/load.py index ad1ad072b..95a2e56ac 100644 --- a/src/core/jupiter/core/smart_lists/use_case/load.py +++ b/src/core/jupiter/core/smart_lists/use_case/load.py @@ -1,33 +1,27 @@ """Use case for loading a smart list.""" -from typing import cast - -from jupiter.core.common.sub.notes.collection import NoteCollection -from jupiter.core.common.sub.notes.root import Note, NoteRepository -from jupiter.core.common.sub.tags.root import TagDomain -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.core.smart_lists.root import SmartList -from jupiter.core.smart_lists.sub.item.root import SmartListItem +from jupiter.core.smart_lists.service.load import ( + SmartListLoadResult, + SmartListLoadService, +) from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import ( readonly_use_case, ) from jupiter.framework.use_case_io import ( UseCaseArgsBase, - UseCaseResultBase, use_case_args, - use_case_result, ) +__all__ = ["SmartListLoadArgs", "SmartListLoadResult", "SmartListLoadUseCase"] + @use_case_args class SmartListLoadArgs(UseCaseArgsBase): @@ -40,18 +34,6 @@ class SmartListLoadArgs(UseCaseArgsBase): include_item_tags_and_notes: bool | None = None -@use_case_result -class SmartListLoadResult(UseCaseResultBase): - """SmartListLoadResult.""" - - smart_list: SmartList - tags: list[Tag] - note: Note | None - smart_list_items: list[SmartListItem] - smart_list_item_generic_tags: dict[EntityId, list[Tag]] | None - smart_list_item_notes: list[Note] | None - - @readonly_use_case(WorkspaceFeature.SMART_LISTS) class SmartListLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[SmartListLoadArgs, SmartListLoadResult] @@ -74,80 +56,13 @@ async def _perform_transactional_read( args.ref_id, allow_archived=allow_archived, ) - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.SMART_LIST.value, smart_list.ref_id), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=allow_archived_tags, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - smart_list_items = await uow.get_for(SmartListItem).find_all_generic( - parent_ref_id=smart_list.ref_id, allow_archived=allow_archived_items - ) - note = await uow.get(NoteRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.SMART_LIST.value, smart_list.ref_id), + return await SmartListLoadService().do_it( + uow, + context.workspace.ref_id, + smart_list, allow_archived=allow_archived, - ) - - smart_list_item_notes: list[Note] | None = None - smart_list_item_generic_tags: dict[EntityId, list[Tag]] | None = None - if include_item_tags_and_notes and len(smart_list_items) > 0: - note_collection = await uow.get_for(NoteCollection).load_by_parent( - context.workspace.ref_id, - ) - smart_list_item_notes = await uow.get( - NoteRepository - ).find_all_for_note_collection( - note_collection_ref_id=note_collection.ref_id, - allow_archived=allow_archived, - filter_owners=[ - EntityLink.std(NamedEntityTag.SMART_LIST_ITEM.value, rid) - for rid in [item.ref_id for item in smart_list_items] - ], - ) - tags_domain = await uow.get_for(TagDomain).load_by_parent( - context.workspace.ref_id, - ) - item_tag_links = await uow.get(TagLinkRepository).find_all_generic( - parent_ref_id=tags_domain.ref_id, - allow_archived=allow_archived_tags, - owner=[ - EntityLink.std(NamedEntityTag.SMART_LIST_ITEM.value, item.ref_id) - for item in smart_list_items - ], - ) - all_item_tag_ref_ids: list[EntityId] = [] - for tl in item_tag_links: - all_item_tag_ref_ids.extend(tl.ref_ids) - if all_item_tag_ref_ids: - all_item_tags = await uow.get_for(Tag).find_all_generic( - parent_ref_id=tags_domain.ref_id, - allow_archived=allow_archived_tags, - ref_id=list(set(all_item_tag_ref_ids)), - ) - all_item_tags_by_ref_id = {t.ref_id: t for t in all_item_tags} - else: - all_item_tags_by_ref_id = {} - - smart_list_item_generic_tags = { - cast(EntityId, tl.owner.ref_id): [ - all_item_tags_by_ref_id[rid] - for rid in tl.ref_ids - if rid in all_item_tags_by_ref_id - ] - for tl in item_tag_links - } - - return SmartListLoadResult( - smart_list=smart_list, - tags=tags, - note=note, - smart_list_items=smart_list_items, - smart_list_item_generic_tags=smart_list_item_generic_tags, - smart_list_item_notes=smart_list_item_notes, + allow_archived_items=allow_archived_items, + allow_archived_tags=allow_archived_tags, + include_item_tags_and_notes=include_item_tags_and_notes, ) diff --git a/src/core/jupiter/core/smart_lists/use_case/load_public.py b/src/core/jupiter/core/smart_lists/use_case/load_public.py new file mode 100644 index 000000000..51f632099 --- /dev/null +++ b/src/core/jupiter/core/smart_lists/use_case/load_public.py @@ -0,0 +1,84 @@ +"""Guest readonly use case for loading a published smart list.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.smart_lists.collection import SmartListCollection +from jupiter.core.smart_lists.root import SmartList +from jupiter.core.smart_lists.service.load import ( + SmartListLoadResult, + SmartListLoadService, +) +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class SmartListLoadPublicArgs(UseCaseArgsBase): + """SmartListLoadPublic args.""" + + external_id: PublishExternalId + include_item_tags_and_notes: bool | None + + +class SmartListLoadPublicUseCase( + JupiterGuestReadonlyUseCase[SmartListLoadPublicArgs, SmartListLoadResult] +): + """Load a published smart list by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: SmartListLoadPublicArgs, + ) -> SmartListLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.SMART_LIST.value: + raise InputValidationError( + "The publish entity does not refer to a smart list." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + smart_list_collection = await uow.get_for( + SmartListCollection + ).load_by_parent(publish_domain.workspace.ref_id) + smart_list = await uow.get_for(SmartList).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if smart_list.parent_ref_id != smart_list_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace smart list." + ) + + return await SmartListLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + smart_list, + allow_archived=False, + allow_archived_items=False, + allow_archived_tags=False, + include_item_tags_and_notes=args.include_item_tags_and_notes or True, + include_publish_entity=False, + ) diff --git a/src/webui/app/rendering/published-loader.server.ts b/src/webui/app/rendering/published-loader.server.ts new file mode 100644 index 000000000..c938f4eba --- /dev/null +++ b/src/webui/app/rendering/published-loader.server.ts @@ -0,0 +1,42 @@ +import { ApiError } from "@jupiter/webapi-client"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { ZodError } from "zod"; + +/** Re-throw loader errors so Remix route error boundaries can handle them. */ +export function handlePublishedLoaderError(error: unknown): never { + if (error instanceof Response) { + throw error; + } + + if (error instanceof ApiError) { + if ( + error.status === StatusCodes.NOT_FOUND || + error.status === StatusCodes.UNPROCESSABLE_ENTITY + ) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + throw new Response(ReasonPhrases.INTERNAL_SERVER_ERROR, { + status: StatusCodes.INTERNAL_SERVER_ERROR, + statusText: error.message, + }); + } + + if (error instanceof ZodError) { + throw new Response(ReasonPhrases.NOT_FOUND, { + status: StatusCodes.NOT_FOUND, + statusText: ReasonPhrases.NOT_FOUND, + }); + } + + if (error instanceof Error) { + throw error; + } + + throw new Response(ReasonPhrases.INTERNAL_SERVER_ERROR, { + status: StatusCodes.INTERNAL_SERVER_ERROR, + }); +} diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx index c7fe347cc..feb96c299 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -1,4 +1,4 @@ -import { ApiError, NamedEntityTag } from "@jupiter/webapi-client"; +import { NamedEntityTag } from "@jupiter/webapi-client"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { redirect } from "@remix-run/node"; import { ReasonPhrases, StatusCodes } from "http-status-codes"; @@ -8,6 +8,7 @@ import { parseEntityLinkStd } from "@jupiter/core/common/entity-link"; import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; const ParamsSchema = z.object({ externalId: z.string(), @@ -29,8 +30,10 @@ function publishedEntityLocation(externalId: string, owner: string): string { return `/app/public/published/schedule-event-in-day/${externalId}`; case NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS: return `/app/public/published/schedule-event-full-days/${externalId}`; + case NamedEntityTag.SMART_LIST: + return `/app/public/published/smart-list/${externalId}`; case NamedEntityTag.SMART_LIST_ITEM: - return `/app/public/published/smart-list-item/${externalId}`; + return `/app/public/published/smart-list/item/${externalId}`; case NamedEntityTag.METRIC_ENTRY: return `/app/public/published/metric-entry/${externalId}`; case NamedEntityTag.DOC: @@ -52,10 +55,10 @@ function publishedEntityLocation(externalId: string, owner: string): string { } export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.publish.publishEntityLoadByExternalId({ external_id: externalId, }); @@ -64,14 +67,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { publishedEntityLocation(externalId, result.publish_entity.owner), ); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx b/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx index 7805102a6..6602664aa 100644 --- a/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx @@ -1,9 +1,7 @@ import type { BigPlanLoadResult, InboxTask } from "@jupiter/webapi-client"; -import { ApiError } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext, useMemo } from "react"; import { z } from "zod"; import { parseParams, parseQuery } from "zodix"; @@ -20,6 +18,7 @@ import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansio import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -38,11 +37,11 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const query = parseQuery(request, QuerySchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const query = parseQuery(request, QuerySchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.bigPlans.bigPlanLoadPublic({ external_id: externalId, inbox_task_retrieve_offset: query.inboxTasksRetrieveOffset, @@ -63,14 +62,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { inboxTasksPageSize: result.inbox_tasks_page_size, }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/chore/$externalId.tsx b/src/webui/app/routes/app/public/published/chore/$externalId.tsx index 0e4524a61..09fb356e7 100644 --- a/src/webui/app/routes/app/public/published/chore/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/chore/$externalId.tsx @@ -1,9 +1,7 @@ import type { InboxTask } from "@jupiter/webapi-client"; -import { ApiError } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext, useMemo } from "react"; import { z } from "zod"; import { parseParams, parseQuery } from "zodix"; @@ -19,6 +17,7 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { ChoreEditor } from "@jupiter/core/chores/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -37,11 +36,11 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const query = parseQuery(request, QuerySchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const query = parseQuery(request, QuerySchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.chores.choreLoadPublic({ external_id: externalId, inbox_task_retrieve_offset: query.inboxTasksRetrieveOffset, @@ -60,14 +59,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { inboxTasksPageSize: result.inbox_tasks_page_size, }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/doc/$externalId.tsx b/src/webui/app/routes/app/public/published/doc/$externalId.tsx index a7b4c0a34..2ed54d419 100644 --- a/src/webui/app/routes/app/public/published/doc/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/doc/$externalId.tsx @@ -1,7 +1,6 @@ -import { ApiError, NamedEntityTag } from "@jupiter/webapi-client"; +import { NamedEntityTag } from "@jupiter/webapi-client"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { z } from "zod"; import { parseParams } from "zodix"; import { entityLinkStd } from "@jupiter/core/common/entity-link"; @@ -14,6 +13,7 @@ import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansio import { DocEditor } from "@jupiter/core/docs/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -25,10 +25,10 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.docs.docLoadPublic({ external_id: externalId, }); @@ -39,14 +39,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { tags: result.tags ?? [], }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/habit/$externalId.tsx b/src/webui/app/routes/app/public/published/habit/$externalId.tsx index 982cba85a..dfa0912d9 100644 --- a/src/webui/app/routes/app/public/published/habit/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/habit/$externalId.tsx @@ -1,9 +1,7 @@ import type { InboxTask } from "@jupiter/webapi-client"; -import { ApiError } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { DateTime } from "luxon"; import { useContext, useMemo } from "react"; import { z } from "zod"; @@ -20,6 +18,7 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { HabitEditor } from "@jupiter/core/habits/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -40,18 +39,19 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const query = parseQuery(request, QuerySchema); - const apiClient = await getGuestApiClient(request); + try { + const { externalId } = parseParams(params, ParamsSchema); + const query = parseQuery(request, QuerySchema); + const apiClient = await getGuestApiClient(request); - let earliestDate = query.viewOneIncludeStreakMarksEarliestDate; - let latestDate = query.viewOneIncludeStreakMarksLatestDate; - if (earliestDate === undefined) { - earliestDate = DateTime.now().minus({ days: 90 }).toISODate() ?? undefined; - latestDate = DateTime.now().toISODate() ?? undefined; - } + let earliestDate = query.viewOneIncludeStreakMarksEarliestDate; + let latestDate = query.viewOneIncludeStreakMarksLatestDate; + if (earliestDate === undefined) { + earliestDate = + DateTime.now().minus({ days: 90 }).toISODate() ?? undefined; + latestDate = DateTime.now().toISODate() ?? undefined; + } - try { const result = await apiClient.habits.habitLoadPublic({ external_id: externalId, inbox_task_retrieve_offset: query.inboxTasksRetrieveOffset, @@ -75,14 +75,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { streakMarkLatestDate: result.streak_mark_latest_date, }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/journal/$externalId.tsx b/src/webui/app/routes/app/public/published/journal/$externalId.tsx index c050a5c24..56064d7ca 100644 --- a/src/webui/app/routes/app/public/published/journal/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/journal/$externalId.tsx @@ -1,8 +1,6 @@ -import { ApiError } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseParams } from "zodix"; @@ -18,6 +16,7 @@ import { allowUserChanges } from "@jupiter/core/journals/source"; import { ShowReport } from "@jupiter/core/report/component/show-report"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -29,10 +28,10 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.journals.journalLoadPublic({ external_id: externalId, }); @@ -44,14 +43,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { tags: result.tags ?? [], }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/metric-entry/$externalId.tsx b/src/webui/app/routes/app/public/published/metric-entry/$externalId.tsx index 9c35c2e3c..f5e444e29 100644 --- a/src/webui/app/routes/app/public/published/metric-entry/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/metric-entry/$externalId.tsx @@ -1,8 +1,6 @@ -import { ApiError } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseParams } from "zodix"; @@ -16,6 +14,7 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { MetricEntryEditor } from "@jupiter/core/metrics/sub/entry/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -27,10 +26,10 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.metrics.metricEntryLoadPublic({ external_id: externalId, }); @@ -42,14 +41,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { note: result.note ?? null, }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/person/$externalId.tsx b/src/webui/app/routes/app/public/published/person/$externalId.tsx index 8c5eb77c4..f3d49856f 100644 --- a/src/webui/app/routes/app/public/published/person/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/person/$externalId.tsx @@ -1,8 +1,6 @@ -import { ApiError } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseParams } from "zodix"; @@ -17,6 +15,7 @@ import { PersonEditor } from "@jupiter/core/prm/sub/person/component/editor"; import { OccasionStack } from "@jupiter/core/prm/sub/person/sub/occasion/components/stack"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -28,10 +27,10 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.prm.personLoadPublic({ external_id: externalId, }); @@ -47,14 +46,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { note: result.note ?? null, }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx b/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx index 9a1534c15..f292899cd 100644 --- a/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx @@ -1,8 +1,6 @@ -import { ApiError } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseParams } from "zodix"; @@ -17,6 +15,7 @@ import { ScheduleEventFullDaysEditor } from "@jupiter/core/schedule/sub/event_fu import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_full_days/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -28,10 +27,10 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.schedule.scheduleEventFullDaysLoadPublic({ external_id: externalId, }); @@ -45,14 +44,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { contacts: result.contacts ?? [], }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx b/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx index 39093ff39..1a22ad616 100644 --- a/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx @@ -1,8 +1,6 @@ -import { ApiError } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext, useMemo } from "react"; import { z } from "zod"; import { parseParams } from "zodix"; @@ -18,6 +16,7 @@ import { ScheduleEventInDayEditor } from "@jupiter/core/schedule/sub/event_in_da import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_in_day/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -29,10 +28,10 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.schedule.scheduleEventInDayLoadPublic({ external_id: externalId, }); @@ -46,14 +45,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { contacts: result.contacts ?? [], }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/smart-list/$externalId.tsx b/src/webui/app/routes/app/public/published/smart-list/$externalId.tsx new file mode 100644 index 000000000..690156897 --- /dev/null +++ b/src/webui/app/routes/app/public/published/smart-list/$externalId.tsx @@ -0,0 +1,229 @@ +import type { Contact, Tag } from "@jupiter/webapi-client"; +import { DocsHelpSubject } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { Outlet } from "@remix-run/react"; +import { AnimatePresence } from "framer-motion"; +import { useContext, useMemo, useState } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import Check from "@jupiter/core/infra/component/check"; +import { EntityNameComponent } from "@jupiter/core/common/component/entity-name"; +import { EntityNoNothingCard } from "@jupiter/core/infra/component/entity-no-nothing-card"; +import { + EntityCard, + EntityLink, +} from "@jupiter/core/infra/component/entity-card"; +import { EntityStack } from "@jupiter/core/infra/component/entity-stack"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { NestingAwareBlock } from "@jupiter/core/infra/component/layout/nesting-aware-block"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { TagTag } from "@jupiter/core/common/sub/tags/component/tag-tag"; +import { ContactTag } from "@jupiter/core/common/sub/contacts/component/contact-tag"; +import { + DisplayType, + useLeafNeedsToShowLeaflet, +} from "@jupiter/core/infra/component/use-nested-entities"; +import { + FilterManyOptions, + SectionActions, +} from "@jupiter/core/infra/component/section-actions"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + const response = await apiClient.smartLists.smartListLoadPublic({ + external_id: externalId, + include_item_tags_and_notes: true, + }); + + const genericTagsByItemRefId: { [key: string]: Array<Tag> } = + response.smart_list_item_generic_tags ?? {}; + const contactsByItemRefId: { [key: string]: Array<Contact> } = + response.smart_list_item_contacts ?? {}; + + const allItemTags = Object.values(genericTagsByItemRefId) + .flat() + .filter( + (tag, index, tags) => + tags.findIndex((other) => other.ref_id === tag.ref_id) === index, + ); + const allContacts = Object.values(contactsByItemRefId) + .flat() + .filter( + (contact, index, contacts) => + contacts.findIndex((other) => other.ref_id === contact.ref_id) === + index, + ); + + return json({ + externalId, + smartList: response.smart_list, + smartListItems: response.smart_list_items, + allItemTags, + allContacts, + genericTagsByItemRefId, + contactsByItemRefId, + }); + } catch (error) { + handlePublishedLoaderError(error); + } +} + +export default function PublishedSmartList() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const shouldShowALeaflet = useLeafNeedsToShowLeaflet(); + + const [selectedDoneness, setSelectedDoneness] = useState<boolean[]>([]); + const [selectedTagsRefId, setSelectedTagsRefId] = useState<string[]>([]); + const [selectedContactsRefId, setSelectedContactsRefId] = useState<string[]>( + [], + ); + + const filteredSmartListItems = useMemo( + () => + loaderData.smartListItems.filter((item) => { + const doneOk = + selectedDoneness.length === 0 || + selectedDoneness.includes(item.is_done); + + const tags = loaderData.genericTagsByItemRefId[item.ref_id] ?? []; + const tagsOk = + selectedTagsRefId.length === 0 || + tags.some((tag) => selectedTagsRefId.includes(tag.ref_id)); + + const contacts = loaderData.contactsByItemRefId[item.ref_id] ?? []; + const contactsOk = + selectedContactsRefId.length === 0 || + contacts.some((contact: Contact) => + selectedContactsRefId.includes(contact.ref_id), + ); + + return doneOk && tagsOk && contactsOk; + }), + [ + loaderData.smartListItems, + loaderData.genericTagsByItemRefId, + loaderData.contactsByItemRefId, + selectedDoneness, + selectedTagsRefId, + selectedContactsRefId, + ], + ); + + return ( + <LeafPanel + key={`published-smart-list-${loaderData.smartList.ref_id}`} + fakeKey={`published-smart-list-${loaderData.smartList.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + shouldShowALeaflet={shouldShowALeaflet} + > + <NestingAwareBlock shouldHide={shouldShowALeaflet}> + <SectionCard + title={loaderData.smartList.name} + actions={ + <SectionActions + id="published-smart-list-items" + topLevelInfo={topLevelInfo} + inputsEnabled={false} + actions={[ + FilterManyOptions( + "Done", + [ + { value: true, text: "Is done" }, + { value: false, text: "Is not done" }, + ], + setSelectedDoneness, + ), + FilterManyOptions( + "Tags", + loaderData.allItemTags.map((tag) => ({ + value: tag.ref_id, + text: tag.name, + })), + setSelectedTagsRefId, + ), + FilterManyOptions( + "Contacts", + loaderData.allContacts.map((contact) => ({ + value: contact.ref_id, + text: contact.name, + })), + setSelectedContactsRefId, + ), + ]} + /> + } + > + {filteredSmartListItems.length === 0 && ( + <EntityNoNothingCard + title="Nothing To Show" + message="There are no items to show with the current filters." + helpSubject={DocsHelpSubject.SMART_LISTS} + /> + )} + + <EntityStack> + {filteredSmartListItems.map((item) => ( + <EntityCard + key={`smart-list-item-${item.ref_id}`} + entityId={`smart-list-item-${item.ref_id}`} + > + <EntityLink + to={`/app/public/published/smart-list/${loaderData.externalId}/${item.ref_id}`} + > + <EntityNameComponent name={item.name} /> + <Check isDone={item.is_done} /> + {(loaderData.genericTagsByItemRefId[item.ref_id] ?? []).map( + (tag) => ( + <TagTag key={tag.ref_id} tag={tag} /> + ), + )} + {(loaderData.contactsByItemRefId[item.ref_id] ?? []).map( + (contact: Contact) => ( + <ContactTag key={contact.ref_id} contact={contact} /> + ), + )} + </EntityLink> + </EntityCard> + ))} + </EntityStack> + </SectionCard> + </NestingAwareBlock> + + <AnimatePresence mode="wait" initial={false}> + <Outlet /> + </AnimatePresence> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => + `Could not find published smart list ${params.externalId}!`, + error: (params) => + `There was an error loading published smart list ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/public/published/smart-list/$externalId/$itemId.tsx b/src/webui/app/routes/app/public/published/smart-list/$externalId/$itemId.tsx new file mode 100644 index 000000000..b541da8b7 --- /dev/null +++ b/src/webui/app/routes/app/public/published/smart-list/$externalId/$itemId.tsx @@ -0,0 +1,97 @@ +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { useContext } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { SmartListItemEditor } from "@jupiter/core/smart_lists/sub/item/component/editor"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), + itemId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAFLET, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + const { externalId, itemId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + const result = + await apiClient.smartLists.smartListItemLoadPublicFromSmartList({ + external_id: externalId, + ref_id: itemId, + }); + + return json({ + externalId, + item: result.item, + genericTags: result.generic_tags ?? [], + contacts: result.contacts ?? [], + note: result.note ?? null, + }); + } catch (error) { + handlePublishedLoaderError(error); + } +} + +export default function PublishedSmartListItemFromList() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const { item, genericTags, contacts, note } = loaderData; + + return ( + <LeafPanel + key={`published-smart-list-item-${item.ref_id}`} + fakeKey={`published-smart-list-item-${item.ref_id}`} + isLeaflet + inputsEnabled={false} + entityNotEditable={true} + returnLocation={`/app/public/published/smart-list/${loaderData.externalId}`} + > + <SmartListItemEditor + item={item} + genericTags={genericTags} + contacts={contacts} + allTags={genericTags} + allContacts={contacts} + inputsEnabled={false} + topLevelInfo={topLevelInfo} + /> + + <SectionCard title="Note"> + {note ? ( + <EntityNoteEditor initialNote={note} inputsEnabled={false} /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary( + (params) => `/app/public/published/smart-list/${params.externalId}`, + ParamsSchema, + { + notFound: (params) => + `Could not find smart list item ${params.itemId} in published smart list ${params.externalId}!`, + error: (params) => + `There was an error loading smart list item ${params.itemId}! Please try again!`, + }, +); diff --git a/src/webui/app/routes/app/public/published/smart-list-item/$externalId.tsx b/src/webui/app/routes/app/public/published/smart-list/item/$externalId.tsx similarity index 86% rename from src/webui/app/routes/app/public/published/smart-list-item/$externalId.tsx rename to src/webui/app/routes/app/public/published/smart-list/item/$externalId.tsx index e3d62515d..32e267ba9 100644 --- a/src/webui/app/routes/app/public/published/smart-list-item/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/smart-list/item/$externalId.tsx @@ -1,8 +1,6 @@ -import { ApiError } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseParams } from "zodix"; @@ -16,6 +14,7 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { SmartListItemEditor } from "@jupiter/core/smart_lists/sub/item/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -27,10 +26,10 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.smartLists.smartListItemLoadPublic({ external_id: externalId, }); @@ -42,14 +41,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { note: result.note ?? null, }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx b/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx index 175029f76..d336aa626 100644 --- a/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx @@ -4,10 +4,9 @@ import type { TimePlanActivity, TimePlanActivityDoneness, } from "@jupiter/webapi-client"; -import { ApiError, TimePlanActivityFeasability } from "@jupiter/webapi-client"; +import { TimePlanActivityFeasability } from "@jupiter/webapi-client"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext, useMemo } from "react"; import { z } from "zod"; import { parseParams } from "zodix"; @@ -26,6 +25,7 @@ import { allowUserChanges } from "@jupiter/core/time_plans/source"; import { TimePlanListMergedActivities } from "@jupiter/core/time_plans/component/list-merged-activities"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -37,10 +37,10 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.timePlans.timePlanLoadPublic({ external_id: externalId, }); @@ -61,14 +61,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { >, }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx b/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx index 5e92943d7..2eeb06ad7 100644 --- a/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx @@ -1,8 +1,6 @@ -import { ApiError } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseParams } from "zodix"; @@ -16,6 +14,7 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { TodoTaskPropertiesEditor } from "@jupiter/core/todo/components/properties-editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -27,10 +26,10 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.todo.todoTaskLoadPublic({ external_id: externalId, }); @@ -46,14 +45,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { contacts: result.contacts ?? [], }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/public/published/vacation/$externalId.tsx b/src/webui/app/routes/app/public/published/vacation/$externalId.tsx index bd52c6116..af7bf0965 100644 --- a/src/webui/app/routes/app/public/published/vacation/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/vacation/$externalId.tsx @@ -1,8 +1,6 @@ -import { ApiError } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseParams } from "zodix"; @@ -16,6 +14,7 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { VacationEditor } from "@jupiter/core/vacations/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ @@ -27,10 +26,10 @@ export const handle = { }; export async function loader({ request, params }: LoaderFunctionArgs) { - const { externalId } = parseParams(params, ParamsSchema); - const apiClient = await getGuestApiClient(request); - try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + const result = await apiClient.vacations.vacationLoadPublic({ external_id: externalId, }); @@ -43,14 +42,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { contacts: result.contacts ?? [], }); } catch (error) { - if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { - throw new Response(ReasonPhrases.NOT_FOUND, { - status: StatusCodes.NOT_FOUND, - statusText: ReasonPhrases.NOT_FOUND, - }); - } - - throw error; + handlePublishedLoaderError(error); } } diff --git a/src/webui/app/routes/app/workspace/big-plans/$id.tsx b/src/webui/app/routes/app/workspace/big-plans/$id.tsx index 1226a0778..31b40b306 100644 --- a/src/webui/app/routes/app/workspace/big-plans/$id.tsx +++ b/src/webui/app/routes/app/workspace/big-plans/$id.tsx @@ -227,6 +227,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { goal: result.goal, milestones: result.milestones, inboxTasks: result.inbox_tasks, + inboxTasksTotalCnt: result.inbox_tasks_total_cnt, + inboxTasksPageSize: result.inbox_tasks_page_size, tags: result.tags, contacts: ( @@ -462,6 +464,8 @@ export default function BigPlan() { goal: loaderData.goal, milestones: loaderData.milestones, inbox_tasks: loaderData.inboxTasks, + inbox_tasks_total_cnt: loaderData.inboxTasksTotalCnt, + inbox_tasks_page_size: loaderData.inboxTasksPageSize, tags: loaderData.tags, contacts: loaderData.contacts, note: loaderData.note, diff --git a/src/webui/app/routes/app/workspace/core/publish/$publishEntityId.tsx b/src/webui/app/routes/app/workspace/core/publish/$publishEntityId.tsx index 37699d637..427fd43d0 100644 --- a/src/webui/app/routes/app/workspace/core/publish/$publishEntityId.tsx +++ b/src/webui/app/routes/app/workspace/core/publish/$publishEntityId.tsx @@ -16,7 +16,6 @@ import { PublishPanel } from "#/core/common/sub/publish/components/publish-panel import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; import { GlobalError } from "@jupiter/core/infra/component/errors"; import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; -import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; diff --git a/src/webui/app/routes/app/workspace/smart-lists/$id.tsx b/src/webui/app/routes/app/workspace/smart-lists/$id.tsx index fa4a25d4e..9d0e5022b 100644 --- a/src/webui/app/routes/app/workspace/smart-lists/$id.tsx +++ b/src/webui/app/routes/app/workspace/smart-lists/$id.tsx @@ -39,6 +39,7 @@ import { FilterManyOptions, SectionActions, } from "@jupiter/core/infra/component/section-actions"; +import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -56,6 +57,18 @@ const UpdateSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -82,23 +95,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { allow_archived: false, }); - const itemContactPairs = await Promise.all( - response.smart_list_items.map(async (item) => { - const itemLoadResult = await apiClient.smartLists.smartListItemLoad({ - ref_id: item.ref_id, - allow_archived: true, - }); - return [ - item.ref_id, - itemLoadResult.contacts as Array<Contact>, - ] as const; - }), - ); - const contactsByItemRefId: { [key: string]: Array<Contact> } = - Object.fromEntries(itemContactPairs); - const genericTagsByItemRefId: { [key: string]: Array<Tag> } = response.smart_list_item_generic_tags ?? {}; + const contactsByItemRefId: { [key: string]: Array<Contact> } = + response.smart_list_item_contacts ?? {}; return json({ smartList: response.smart_list, @@ -107,6 +107,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { allContacts: allContacts.contacts as Array<Contact>, genericTagsByItemRefId, contactsByItemRefId, + publishEntity: response.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -125,22 +126,64 @@ export async function action({ request, params }: LoaderFunctionArgs) { const { id } = parseParams(params, ParamsSchema); const form = await parseForm(request, UpdateSchema); - switch (form.intent) { - case "archive": { - await apiClient.smartLists.smartListArchive({ - ref_id: id, - }); + try { + switch (form.intent) { + case "archive": { + await apiClient.smartLists.smartListArchive({ + ref_id: id, + }); - return redirect("/app/workspace/smart-lists"); - } + return redirect("/app/workspace/smart-lists"); + } - case "remove": { - await apiClient.smartLists.smartListRemove({ - ref_id: id, - }); + case "remove": { + await apiClient.smartLists.smartListRemove({ + ref_id: id, + }); + + return redirect("/app/workspace/smart-lists"); + } - return redirect("/app/workspace/smart-lists"); + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/smart-lists/${id}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/smart-lists/${id}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/smart-lists/${id}`); + } + + default: + throw new Response("Bad Intent", { status: 500 }); + } + } catch (error) { + if ( + error instanceof ApiError && + error.status === StatusCodes.UNPROCESSABLE_ENTITY + ) { + return json(validationErrorToUIErrorInfo(error.body)); } + + if (error instanceof ApiError && error.status === StatusCodes.CONFLICT) { + return json(validationErrorToUIErrorInfo(error.body)); + } + + throw error; } } @@ -193,6 +236,8 @@ export default function SmartListViewItems() { entityArchived={loaderData.smartList.archived} createLocation={`/app/workspace/smart-lists/${loaderData.smartList.ref_id}/new`} returnLocation="/app/workspace/smart-lists" + publishable + publishEntity={loaderData.publishEntity ?? undefined} actions={ <SectionActions id="smart-list-items" diff --git a/src/webui/app/routes/app/workspace/smart-lists/$id/$itemId.tsx b/src/webui/app/routes/app/workspace/smart-lists/$id/$itemId.tsx index f903d16a7..9ec3c62bf 100644 --- a/src/webui/app/routes/app/workspace/smart-lists/$id/$itemId.tsx +++ b/src/webui/app/routes/app/workspace/smart-lists/$id/$itemId.tsx @@ -202,6 +202,10 @@ export async function action({ request, params }: ActionFunctionArgs) { return json(validationErrorToUIErrorInfo(error.body)); } + if (error instanceof ApiError && error.status === StatusCodes.CONFLICT) { + return json(validationErrorToUIErrorInfo(error.body)); + } + throw error; } } From 76fc04375b8e93d00755afabe8ce3c4ee0593d54 Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Mon, 8 Jun 2026 14:44:13 +0300 Subject: [PATCH 11/21] Handled metrics --- .../metric_entry_load_public_from_metric.py | 216 ++++++++++ .../api/metrics/metric_load_public.py | 212 ++++++++++ .../jupiter_webapi_client/models/__init__.py | 4 + ...tric_entry_load_public_from_metric_args.py | 70 ++++ .../models/metric_load_args.py | 22 ++ .../models/metric_load_public_args.py | 84 ++++ gen/ts/webapi-client/gen/index.ts | 2 + .../MetricEntryLoadPublicFromMetricArgs.ts | 13 + .../gen/models/MetricLoadArgs.ts | 1 + .../gen/models/MetricLoadPublicArgs.ts | 12 + .../gen/models/MetricLoadResult.ts | 4 + .../gen/services/MetricsService.ts | 58 +++ itests/webui/entities/metrics.test.py | 71 +++- .../common/sub/publish/sub/entity/root.py | 2 +- src/core/jupiter/core/metrics/root.py | 4 + src/core/jupiter/core/metrics/service/load.py | 276 +++++++++++++ .../entry/use_case/load_public_from_metric.py | 94 +++++ .../jupiter/core/metrics/use_case/load.py | 162 ++------ .../core/metrics/use_case/load_public.py | 87 ++++ .../app/public/published/$externalId.tsx | 4 +- .../public/published/metric/$externalId.tsx | 372 ++++++++++++++++++ .../published/metric/$externalId/$entryId.tsx | 96 +++++ .../entry}/$externalId.tsx | 0 .../app/routes/app/workspace/metrics/$id.tsx | 54 ++- 24 files changed, 1763 insertions(+), 157 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public_from_metric.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_public_from_metric_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/metric_load_public_args.py create mode 100644 gen/ts/webapi-client/gen/models/MetricEntryLoadPublicFromMetricArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/MetricLoadPublicArgs.ts create mode 100644 src/core/jupiter/core/metrics/service/load.py create mode 100644 src/core/jupiter/core/metrics/sub/entry/use_case/load_public_from_metric.py create mode 100644 src/core/jupiter/core/metrics/use_case/load_public.py create mode 100644 src/webui/app/routes/app/public/published/metric/$externalId.tsx create mode 100644 src/webui/app/routes/app/public/published/metric/$externalId/$entryId.tsx rename src/webui/app/routes/app/public/published/{metric-entry => metric/entry}/$externalId.tsx (100%) diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public_from_metric.py b/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public_from_metric.py new file mode 100644 index 000000000..fb56d56dc --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public_from_metric.py @@ -0,0 +1,216 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.metric_entry_load_public_from_metric_args import MetricEntryLoadPublicFromMetricArgs +from ...models.metric_entry_load_result import MetricEntryLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: MetricEntryLoadPublicFromMetricArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/metric-entry-load-public-from-metric", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | MetricEntryLoadResult | None: + if response.status_code == 200: + response_200 = MetricEntryLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | MetricEntryLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: MetricEntryLoadPublicFromMetricArgs | Unset = UNSET, +) -> Response[ErrorResponse | MetricEntryLoadResult]: + """Load a smart list item through a published smart list. + + Args: + body (MetricEntryLoadPublicFromMetricArgs | Unset): + MetricEntryLoadPublicFromMetric args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | MetricEntryLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: MetricEntryLoadPublicFromMetricArgs | Unset = UNSET, +) -> ErrorResponse | MetricEntryLoadResult | None: + """Load a smart list item through a published smart list. + + Args: + body (MetricEntryLoadPublicFromMetricArgs | Unset): + MetricEntryLoadPublicFromMetric args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | MetricEntryLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: MetricEntryLoadPublicFromMetricArgs | Unset = UNSET, +) -> Response[ErrorResponse | MetricEntryLoadResult]: + """Load a smart list item through a published smart list. + + Args: + body (MetricEntryLoadPublicFromMetricArgs | Unset): + MetricEntryLoadPublicFromMetric args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | MetricEntryLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: MetricEntryLoadPublicFromMetricArgs | Unset = UNSET, +) -> ErrorResponse | MetricEntryLoadResult | None: + """Load a smart list item through a published smart list. + + Args: + body (MetricEntryLoadPublicFromMetricArgs | Unset): + MetricEntryLoadPublicFromMetric args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | MetricEntryLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_load_public.py new file mode 100644 index 000000000..10c99483b --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.metric_load_public_args import MetricLoadPublicArgs +from ...models.metric_load_result import MetricLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: MetricLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/metric-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | MetricLoadResult | None: + if response.status_code == 200: + response_200 = MetricLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | MetricLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: MetricLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | MetricLoadResult]: + """Load a published smart list by publish external id. + + Args: + body (MetricLoadPublicArgs | Unset): MetricLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | MetricLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: MetricLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | MetricLoadResult | None: + """Load a published smart list by publish external id. + + Args: + body (MetricLoadPublicArgs | Unset): MetricLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | MetricLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: MetricLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | MetricLoadResult]: + """Load a published smart list by publish external id. + + Args: + body (MetricLoadPublicArgs | Unset): MetricLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | MetricLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: MetricLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | MetricLoadResult | None: + """Load a published smart list by publish external id. + + Args: + body (MetricLoadPublicArgs | Unset): MetricLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | MetricLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index 2e97b04bd..9d00b5867 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -508,8 +508,10 @@ from .metric_find_args import MetricFindArgs from .metric_find_response_entry import MetricFindResponseEntry from .metric_find_result import MetricFindResult +from .metric_entry_load_public_from_metric_args import MetricEntryLoadPublicFromMetricArgs from .metric_load_args import MetricLoadArgs from .metric_load_metric_entry_tags import MetricLoadMetricEntryTags +from .metric_load_public_args import MetricLoadPublicArgs from .metric_load_result import MetricLoadResult from .metric_load_settings_args import MetricLoadSettingsArgs from .metric_load_settings_result import MetricLoadSettingsResult @@ -1524,7 +1526,9 @@ "MetricFindArgs", "MetricFindResponseEntry", "MetricFindResult", + "MetricEntryLoadPublicFromMetricArgs", "MetricLoadArgs", + "MetricLoadPublicArgs", "MetricLoadMetricEntryTags", "MetricLoadResult", "MetricLoadSettingsArgs", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_public_from_metric_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_public_from_metric_args.py new file mode 100644 index 000000000..068a06f22 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/metric_entry_load_public_from_metric_args.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="MetricEntryLoadPublicFromMetricArgs") + + +@_attrs_define +class MetricEntryLoadPublicFromMetricArgs: + """MetricEntryLoadPublicFromMetric args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + ref_id (str): A generic entity id. + """ + + external_id: str + ref_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + ref_id = self.ref_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + "ref_id": ref_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + ref_id = d.pop("ref_id") + + metric_entry_load_public_from_metric_args = cls( + external_id=external_id, + ref_id=ref_id, + ) + + metric_entry_load_public_from_metric_args.additional_properties = d + return metric_entry_load_public_from_metric_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_args.py index fa1e5bb4f..1528fef1f 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_args.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_args.py @@ -19,12 +19,14 @@ class MetricLoadArgs: ref_id (str): A generic entity id. allow_archived (bool | None | Unset): allow_archived_entries (bool | None | Unset): + include_entry_tags_and_contacts (bool | None | Unset): collection_task_retrieve_offset (int | None | Unset): """ ref_id: str allow_archived: bool | None | Unset = UNSET allow_archived_entries: bool | None | Unset = UNSET + include_entry_tags_and_contacts: bool | None | Unset = UNSET collection_task_retrieve_offset: int | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -43,6 +45,12 @@ def to_dict(self) -> dict[str, Any]: else: allow_archived_entries = self.allow_archived_entries + include_entry_tags_and_contacts: bool | None | Unset + if isinstance(self.include_entry_tags_and_contacts, Unset): + include_entry_tags_and_contacts = UNSET + else: + include_entry_tags_and_contacts = self.include_entry_tags_and_contacts + collection_task_retrieve_offset: int | None | Unset if isinstance(self.collection_task_retrieve_offset, Unset): collection_task_retrieve_offset = UNSET @@ -60,6 +68,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["allow_archived"] = allow_archived if allow_archived_entries is not UNSET: field_dict["allow_archived_entries"] = allow_archived_entries + if include_entry_tags_and_contacts is not UNSET: + field_dict["include_entry_tags_and_contacts"] = include_entry_tags_and_contacts if collection_task_retrieve_offset is not UNSET: field_dict["collection_task_retrieve_offset"] = collection_task_retrieve_offset @@ -88,6 +98,17 @@ def _parse_allow_archived_entries(data: object) -> bool | None | Unset: allow_archived_entries = _parse_allow_archived_entries(d.pop("allow_archived_entries", UNSET)) + def _parse_include_entry_tags_and_contacts(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + include_entry_tags_and_contacts = _parse_include_entry_tags_and_contacts( + d.pop("include_entry_tags_and_contacts", UNSET) + ) + def _parse_collection_task_retrieve_offset(data: object) -> int | None | Unset: if data is None: return data @@ -103,6 +124,7 @@ def _parse_collection_task_retrieve_offset(data: object) -> int | None | Unset: ref_id=ref_id, allow_archived=allow_archived, allow_archived_entries=allow_archived_entries, + include_entry_tags_and_contacts=include_entry_tags_and_contacts, collection_task_retrieve_offset=collection_task_retrieve_offset, ) diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_public_args.py new file mode 100644 index 000000000..6ca4c82bc --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_public_args.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="MetricLoadPublicArgs") + + +@_attrs_define +class MetricLoadPublicArgs: + """MetricLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + include_entry_tags_and_contacts (bool | None | Unset): + """ + + external_id: str + include_entry_tags_and_contacts: bool | None | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + include_entry_tags_and_contacts: bool | None | Unset + if isinstance(self.include_entry_tags_and_contacts, Unset): + include_entry_tags_and_contacts = UNSET + else: + include_entry_tags_and_contacts = self.include_entry_tags_and_contacts + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + if include_entry_tags_and_contacts is not UNSET: + field_dict["include_entry_tags_and_contacts"] = include_entry_tags_and_contacts + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + def _parse_include_entry_tags_and_contacts(data: object) -> bool | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(bool | None | Unset, data) + + include_entry_tags_and_contacts = _parse_include_entry_tags_and_contacts(d.pop("include_entry_tags_and_contacts", UNSET)) + + metric_load_public_args = cls( + external_id=external_id, + include_entry_tags_and_contacts=include_entry_tags_and_contacts, + ) + + metric_load_public_args.additional_properties = d + return metric_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index 67e461209..38d8edfe1 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -405,6 +405,7 @@ export type { MetricEntryCreateArgs } from './models/MetricEntryCreateArgs'; export type { MetricEntryCreateResult } from './models/MetricEntryCreateResult'; export type { MetricEntryLoadArgs } from './models/MetricEntryLoadArgs'; export type { MetricEntryLoadPublicArgs } from './models/MetricEntryLoadPublicArgs'; +export type { MetricEntryLoadPublicFromMetricArgs } from './models/MetricEntryLoadPublicFromMetricArgs'; export type { MetricEntryLoadResult } from './models/MetricEntryLoadResult'; export type { MetricEntryRemoveArgs } from './models/MetricEntryRemoveArgs'; export type { MetricEntryUpdateArgs } from './models/MetricEntryUpdateArgs'; @@ -412,6 +413,7 @@ export type { MetricFindArgs } from './models/MetricFindArgs'; export type { MetricFindResponseEntry } from './models/MetricFindResponseEntry'; export type { MetricFindResult } from './models/MetricFindResult'; export type { MetricLoadArgs } from './models/MetricLoadArgs'; +export type { MetricLoadPublicArgs } from './models/MetricLoadPublicArgs'; export type { MetricLoadMetricEntryTags } from './models/MetricLoadMetricEntryTags'; export type { MetricLoadResult } from './models/MetricLoadResult'; export type { MetricLoadSettingsArgs } from './models/MetricLoadSettingsArgs'; diff --git a/gen/ts/webapi-client/gen/models/MetricEntryLoadPublicFromMetricArgs.ts b/gen/ts/webapi-client/gen/models/MetricEntryLoadPublicFromMetricArgs.ts new file mode 100644 index 000000000..b61c079ef --- /dev/null +++ b/gen/ts/webapi-client/gen/models/MetricEntryLoadPublicFromMetricArgs.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { PublishExternalId } from './PublishExternalId'; +/** + * MetricEntryLoadPublicFromMetric args. + */ +export type MetricEntryLoadPublicFromMetricArgs = { + external_id: PublishExternalId; + ref_id: EntityId; +}; diff --git a/gen/ts/webapi-client/gen/models/MetricLoadArgs.ts b/gen/ts/webapi-client/gen/models/MetricLoadArgs.ts index 1d40f67be..b15e2afa1 100644 --- a/gen/ts/webapi-client/gen/models/MetricLoadArgs.ts +++ b/gen/ts/webapi-client/gen/models/MetricLoadArgs.ts @@ -10,6 +10,7 @@ export type MetricLoadArgs = { ref_id: EntityId; allow_archived?: (boolean | null); allow_archived_entries?: (boolean | null); + include_entry_tags_and_contacts?: (boolean | null); collection_task_retrieve_offset?: (number | null); }; diff --git a/gen/ts/webapi-client/gen/models/MetricLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/MetricLoadPublicArgs.ts new file mode 100644 index 000000000..af97f5d7b --- /dev/null +++ b/gen/ts/webapi-client/gen/models/MetricLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * MetricLoadPublic args. + */ +export type MetricLoadPublicArgs = { + external_id: PublishExternalId; + include_entry_tags_and_contacts?: (boolean | null); +}; diff --git a/gen/ts/webapi-client/gen/models/MetricLoadResult.ts b/gen/ts/webapi-client/gen/models/MetricLoadResult.ts index dc2cea219..674cb02b3 100644 --- a/gen/ts/webapi-client/gen/models/MetricLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/MetricLoadResult.ts @@ -2,11 +2,13 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { Contact } from './Contact'; import type { InboxTask } from './InboxTask'; import type { Metric } from './Metric'; import type { MetricEntry } from './MetricEntry'; import type { MetricLoadMetricEntryTags } from './MetricLoadMetricEntryTags'; import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { Tag } from './Tag'; /** * MetricLoadResult. @@ -17,8 +19,10 @@ export type MetricLoadResult = { tags: Array<Tag>; metric_entries: Array<MetricEntry>; metric_entry_tags: Array<MetricLoadMetricEntryTags>; + metric_entry_contacts?: (Record<string, Array<Contact>> | null); collection_tasks: Array<InboxTask>; collection_tasks_total_cnt: number; collection_tasks_page_size: number; + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/services/MetricsService.ts b/gen/ts/webapi-client/gen/services/MetricsService.ts index 40e534d7b..2d7d56d60 100644 --- a/gen/ts/webapi-client/gen/services/MetricsService.ts +++ b/gen/ts/webapi-client/gen/services/MetricsService.ts @@ -10,12 +10,14 @@ import type { MetricEntryCreateArgs } from '../models/MetricEntryCreateArgs'; import type { MetricEntryCreateResult } from '../models/MetricEntryCreateResult'; import type { MetricEntryLoadArgs } from '../models/MetricEntryLoadArgs'; import type { MetricEntryLoadPublicArgs } from '../models/MetricEntryLoadPublicArgs'; +import type { MetricEntryLoadPublicFromMetricArgs } from '../models/MetricEntryLoadPublicFromMetricArgs'; import type { MetricEntryLoadResult } from '../models/MetricEntryLoadResult'; import type { MetricEntryRemoveArgs } from '../models/MetricEntryRemoveArgs'; import type { MetricEntryUpdateArgs } from '../models/MetricEntryUpdateArgs'; import type { MetricFindArgs } from '../models/MetricFindArgs'; import type { MetricFindResult } from '../models/MetricFindResult'; import type { MetricLoadArgs } from '../models/MetricLoadArgs'; +import type { MetricLoadPublicArgs } from '../models/MetricLoadPublicArgs'; import type { MetricLoadResult } from '../models/MetricLoadResult'; import type { MetricLoadSettingsArgs } from '../models/MetricLoadSettingsArgs'; import type { MetricLoadSettingsResult } from '../models/MetricLoadSettingsResult'; @@ -138,6 +140,34 @@ export class MetricsService { }, }); } + /** + * Load a metric entry through a published metric. + * @param requestBody The input data + * @returns MetricEntryLoadResult Successful response + * @throws ApiError + */ + public metricEntryLoadPublicFromMetric( + requestBody?: MetricEntryLoadPublicFromMetricArgs, + ): CancelablePromise<MetricEntryLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/metric-entry-load-public-from-metric', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * The command for removing a metric entry. * @param requestBody The input data @@ -278,6 +308,34 @@ export class MetricsService { }, }); } + /** + * Load a published metric by publish external id. + * @param requestBody The input data + * @returns MetricLoadResult Successful response + * @throws ApiError + */ + public metricLoadPublic( + requestBody?: MetricLoadPublicArgs, + ): CancelablePromise<MetricLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/metric-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * Use case for loading a metric. * @param requestBody The input data diff --git a/itests/webui/entities/metrics.test.py b/itests/webui/entities/metrics.test.py index 660ce433d..9730591b3 100644 --- a/itests/webui/entities/metrics.test.py +++ b/itests/webui/entities/metrics.test.py @@ -32,7 +32,11 @@ ) from playwright.sync_api import Page, expect -from itests.helpers import get_parsed_from_response, open_leaf_publish_panel +from itests.helpers import ( + get_parsed_from_response, + open_branch_publish_panel, + open_leaf_publish_panel, +) @pytest.fixture(autouse=True, scope="module") @@ -180,8 +184,71 @@ def test_webui_metric_entry_publish_and_view_public( assert "/app/public/published/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/metric-entry/")) + page.wait_for_url(re.compile(r"/app/public/published/metric/entry/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="collectionTime"]')).to_have_value("2024-01-15") expect(page.locator('input[name="value"]')).to_have_value("25") + + +def test_webui_metric_publish_and_view_public( + page: Page, create_metric, create_metric_entry +) -> None: + metric = create_metric("Published Metric", metric_unit=MetricUnit.COUNT) + entry = create_metric_entry(metric.ref_id, 25.0, "2024-01-15") + page.goto(f"/app/workspace/metrics/{metric.ref_id}") + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "Metric-publish") + page.locator("button[id='Metric-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/metrics/{metric.ref_id}")) + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "Metric-publish") + expect(page.locator("#Metric-publish")).to_contain_text("draft") + + page.locator("button[id='Metric-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/metrics/{metric.ref_id}")) + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "Metric-publish") + expect(page.locator("#Metric-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/metric/")) + page.wait_for_selector("#leaf-panel") + + expect(page.locator(f"#metric-entry-{entry.ref_id}")).to_contain_text("25") + + +def test_webui_metric_entry_view_public( + page: Page, create_metric, create_metric_entry +) -> None: + metric = create_metric("Published Metric For Entry", metric_unit=MetricUnit.COUNT) + entry = create_metric_entry(metric.ref_id, 42.0, "2024-02-01") + page.goto(f"/app/workspace/metrics/{metric.ref_id}") + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "Metric-publish") + page.locator("button[id='Metric-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/metrics/{metric.ref_id}")) + page.wait_for_selector("#branch-panel") + + open_branch_publish_panel(page, "Metric-publish") + page.locator("button[id='Metric-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/metrics/{metric.ref_id}")) + page.wait_for_selector("#branch-panel") + + public_url = page.locator('input[name="publicUrl"]').input_value() + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/metric/")) + + page.locator(f"#metric-entry-{entry.ref_id}").click() + page.wait_for_url(re.compile(rf"/app/public/published/metric/[^/]+/{entry.ref_id}")) + page.wait_for_selector("#leaflet-panel") + + expect(page.locator('input[name="collectionTime"]')).to_have_value("2024-02-01") + expect(page.locator('input[name="value"]')).to_have_value("42") diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index 341f2d0c3..85db38b8e 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -40,7 +40,7 @@ NamedEntityTag.VACATION.value, # done NamedEntityTag.SMART_LIST.value, # done NamedEntityTag.SMART_LIST_ITEM.value, # done - NamedEntityTag.METRIC.value, + NamedEntityTag.METRIC.value, # done NamedEntityTag.METRIC_ENTRY.value, # done NamedEntityTag.PERSON.value, # done } diff --git a/src/core/jupiter/core/metrics/root.py b/src/core/jupiter/core/metrics/root.py index 18ff04fac..3737a0db6 100644 --- a/src/core/jupiter/core/metrics/root.py +++ b/src/core/jupiter/core/metrics/root.py @@ -4,6 +4,7 @@ from jupiter.core.common.recurring_task_gen_params import RecurringTaskGenParams from jupiter.core.common.sub.inbox_tasks.root import InboxTask from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.metrics.direction import MetricDirection from jupiter.core.metrics.name import MetricName @@ -48,6 +49,9 @@ class Metric(BranchEntity): TagLink, owner=IsEntityLinkStd(NamedEntityTag.METRIC.value) ) note = OwnsAtMostOne(Note, owner=IsEntityLinkStd(NamedEntityTag.METRIC.value)) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.METRIC.value) + ) @staticmethod @create_entity_action diff --git a/src/core/jupiter/core/metrics/service/load.py b/src/core/jupiter/core/metrics/service/load.py new file mode 100644 index 000000000..f9d0aa778 --- /dev/null +++ b/src/core/jupiter/core/metrics/service/load.py @@ -0,0 +1,276 @@ +"""Shared service for loading a metric and its dependent entities.""" + +from typing import cast + +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLinkRepository +from jupiter.core.common.sub.inbox_tasks.collection import InboxTaskCollection +from jupiter.core.common.sub.inbox_tasks.root import InboxTask, InboxTaskRepository +from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.root import TagDomain +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.metrics.root import Metric +from jupiter.core.metrics.sub.entry.root import MetricEntry +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.errors import InputValidationError +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import ( + UseCaseResultBase, + use_case_result, + use_case_result_part, +) + + +@use_case_result_part +class MetricLoadMetricEntryTags(UseCaseResultBase): + """The tags associated with a metric entry.""" + + metric_entry_ref_id: EntityId + tags: list[Tag] + + +@use_case_result +class MetricLoadResult(UseCaseResultBase): + """MetricLoadResult.""" + + metric: Metric + note: Note | None + tags: list[Tag] + metric_entries: list[MetricEntry] + metric_entry_tags: list[MetricLoadMetricEntryTags] + metric_entry_contacts: dict[EntityId, list[Contact]] | None + collection_tasks: list[InboxTask] + collection_tasks_total_cnt: int + collection_tasks_page_size: int + publish_entity: PublishEntity | None + + +class MetricLoadService: + """Shared service for loading a metric and its dependent entities.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + metric: Metric, + *, + allow_archived: bool = False, + allow_archived_entries: bool = False, + allow_archived_tags: bool = False, + include_entry_tags_and_contacts: bool = False, + include_collection_tasks: bool = True, + collection_task_retrieve_offset: int | None = None, + include_publish_entity: bool = True, + ) -> MetricLoadResult: + """Load a metric and its dependent entities.""" + if ( + collection_task_retrieve_offset is not None + and collection_task_retrieve_offset < 0 + ): + raise InputValidationError("Invalid inbox_task_retrieve_offset") + + metric = await uow.get_for(Metric).load_by_id( + metric.ref_id, + allow_archived=allow_archived, + ) + owner = EntityLink.std(NamedEntityTag.METRIC.value, metric.ref_id) + + metric_entries = await uow.get_for(MetricEntry).find_all( + metric.ref_id, allow_archived=allow_archived_entries + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=owner, + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=allow_archived_tags, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + metric_entry_tags: list[MetricLoadMetricEntryTags] = [] + metric_entry_contacts: dict[EntityId, list[Contact]] | None = None + + if include_entry_tags_and_contacts and len(metric_entries) > 0: + tags_domain = await uow.get_for(TagDomain).load_by_parent( + workspace_ref_id, + ) + entry_tag_links = await uow.get(TagLinkRepository).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=allow_archived_tags, + owner=[ + EntityLink.std(NamedEntityTag.METRIC_ENTRY.value, entry.ref_id) + for entry in metric_entries + ], + ) + entry_tag_links_by_ref_id = { + cast(EntityId, tl.owner.ref_id): tl for tl in entry_tag_links + } + all_entry_tag_ref_ids: list[EntityId] = [] + for tl in entry_tag_links: + all_entry_tag_ref_ids.extend(tl.ref_ids) + if all_entry_tag_ref_ids: + all_entry_tags = await uow.get_for(Tag).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=allow_archived_tags, + ref_id=list(set(all_entry_tag_ref_ids)), + ) + all_entry_tags_by_ref_id = {t.ref_id: t for t in all_entry_tags} + else: + all_entry_tags_by_ref_id = {} + + metric_entry_tags = [ + MetricLoadMetricEntryTags( + metric_entry_ref_id=entry.ref_id, + tags=( + [ + all_entry_tags_by_ref_id[rid] + for rid in entry_tag_links_by_ref_id[entry.ref_id].ref_ids + if rid in all_entry_tags_by_ref_id + ] + if entry.ref_id in entry_tag_links_by_ref_id + else [] + ), + ) + for entry in metric_entries + ] + + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace_ref_id, + ) + entry_contact_links = await uow.get(ContactLinkRepository).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + owner=[ + EntityLink.std(NamedEntityTag.METRIC_ENTRY.value, entry.ref_id) + for entry in metric_entries + ], + ) + all_entry_contact_ref_ids: list[EntityId] = [] + for cl in entry_contact_links: + all_entry_contact_ref_ids.extend(cl.contacts_ref_ids) + if all_entry_contact_ref_ids: + all_entry_contacts = await uow.get_for(Contact).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + ref_id=list(set(all_entry_contact_ref_ids)), + ) + all_entry_contacts_by_ref_id = {c.ref_id: c for c in all_entry_contacts} + else: + all_entry_contacts_by_ref_id = {} + + metric_entry_contacts = { + cast(EntityId, cl.owner.ref_id): [ + all_entry_contacts_by_ref_id[rid] + for rid in cl.contacts_ref_ids + if rid in all_entry_contacts_by_ref_id + ] + for cl in entry_contact_links + } + elif len(metric_entries) > 0: + tags_domain = await uow.get_for(TagDomain).load_by_parent( + workspace_ref_id, + ) + tag_links = await uow.get(TagLinkRepository).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=allow_archived_tags, + owner=[ + EntityLink.std(NamedEntityTag.METRIC_ENTRY.value, e.ref_id) + for e in metric_entries + ], + ) + tag_links_by_entry_ref_id = { + cast(EntityId, tl.owner.ref_id): tl for tl in tag_links + } + tag_ref_ids: list[EntityId] = [] + for tl in tag_links: + tag_ref_ids.extend(tl.ref_ids) + if tag_ref_ids: + all_tags = await uow.get_for(Tag).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=allow_archived_tags, + ref_id=list(set(tag_ref_ids)), + ) + all_tags_by_ref_id = {t.ref_id: t for t in all_tags} + else: + all_tags_by_ref_id = {} + metric_entry_tags = [ + MetricLoadMetricEntryTags( + metric_entry_ref_id=e.ref_id, + tags=( + [ + all_tags_by_ref_id[rid] + for rid in tag_links_by_entry_ref_id[e.ref_id].ref_ids + if rid in all_tags_by_ref_id + ] + if e.ref_id in tag_links_by_entry_ref_id + else [] + ), + ) + for e in metric_entries + ] + + collection_tasks: list[InboxTask] = [] + collection_tasks_total_cnt = 0 + collection_tasks_page_size = InboxTaskRepository.PAGE_SIZE + + if include_collection_tasks: + inbox_task_collection = await uow.get_for( + InboxTaskCollection + ).load_by_parent(workspace_ref_id) + + collection_tasks_total_cnt = await uow.get( + InboxTaskRepository + ).count_all_for_owner( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=True, + owner=owner, + ) + + collection_tasks = await uow.get( + InboxTaskRepository + ).find_all_for_owner_created_desc( + parent_ref_id=inbox_task_collection.ref_id, + allow_archived=True, + owner=owner, + retrieve_offset=collection_task_retrieve_offset or 0, + retrieve_limit=InboxTaskRepository.PAGE_SIZE, + ) + + note = await uow.get(NoteRepository).load_optional_for_owner( + owner, + allow_archived=allow_archived, + ) + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + owner, + allow_archived=allow_archived, + ) + + return MetricLoadResult( + metric=metric, + note=note, + tags=tags, + metric_entries=metric_entries, + metric_entry_tags=metric_entry_tags, + metric_entry_contacts=metric_entry_contacts, + collection_tasks=collection_tasks, + collection_tasks_total_cnt=collection_tasks_total_cnt, + collection_tasks_page_size=collection_tasks_page_size, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/metrics/sub/entry/use_case/load_public_from_metric.py b/src/core/jupiter/core/metrics/sub/entry/use_case/load_public_from_metric.py new file mode 100644 index 000000000..fc585a8a8 --- /dev/null +++ b/src/core/jupiter/core/metrics/sub/entry/use_case/load_public_from_metric.py @@ -0,0 +1,94 @@ +"""Guest readonly use case for loading a metric entry via a published metric.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.metrics.collection import MetricCollection +from jupiter.core.metrics.root import Metric +from jupiter.core.metrics.sub.entry.root import MetricEntry +from jupiter.core.metrics.sub.entry.service.load import ( + MetricEntryLoadResult, + MetricEntryLoadService, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class MetricEntryLoadPublicFromMetricArgs(UseCaseArgsBase): + """MetricEntryLoadPublicFromMetric args.""" + + external_id: PublishExternalId + ref_id: EntityId + + +class MetricEntryLoadPublicFromMetricUseCase( + JupiterGuestReadonlyUseCase[ + MetricEntryLoadPublicFromMetricArgs, MetricEntryLoadResult + ] +): + """Load a metric entry through a published metric.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: MetricEntryLoadPublicFromMetricArgs, + ) -> MetricEntryLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.METRIC.value: + raise InputValidationError( + "The publish entity does not refer to a metric." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + metric_collection = await uow.get_for(MetricCollection).load_by_parent( + publish_domain.workspace.ref_id + ) + metric = await uow.get_for(Metric).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if metric.parent_ref_id != metric_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace metric." + ) + + metric_entry = await uow.get_for(MetricEntry).load_by_id( + args.ref_id, + allow_archived=False, + ) + if metric_entry.metric.ref_id != metric.ref_id: + raise InputValidationError( + "The metric entry does not belong to the published metric." + ) + + return await MetricEntryLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + metric_entry, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/metrics/use_case/load.py b/src/core/jupiter/core/metrics/use_case/load.py index 2d427a7c4..3513039d6 100644 --- a/src/core/jupiter/core/metrics/use_case/load.py +++ b/src/core/jupiter/core/metrics/use_case/load.py @@ -1,41 +1,33 @@ """Use case for loading a metric.""" -from typing import cast - -from jupiter.core.common.sub.inbox_tasks.collection import ( - InboxTaskCollection, -) -from jupiter.core.common.sub.inbox_tasks.root import ( - InboxTask, - InboxTaskRepository, -) -from jupiter.core.common.sub.notes.root import Note, NoteRepository -from jupiter.core.common.sub.tags.root import TagDomain -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature from jupiter.core.metrics.root import Metric -from jupiter.core.metrics.sub.entry.root import MetricEntry -from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.metrics.service.load import ( + MetricLoadMetricEntryTags, + MetricLoadResult, + MetricLoadService, +) from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink -from jupiter.framework.errors import InputValidationError from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import ( readonly_use_case, ) from jupiter.framework.use_case_io import ( UseCaseArgsBase, - UseCaseResultBase, use_case_args, - use_case_result, - use_case_result_part, ) +__all__ = [ + "MetricLoadArgs", + "MetricLoadMetricEntryTags", + "MetricLoadResult", + "MetricLoadUseCase", +] + @use_case_args class MetricLoadArgs(UseCaseArgsBase): @@ -45,28 +37,7 @@ class MetricLoadArgs(UseCaseArgsBase): allow_archived: bool | None allow_archived_entries: bool | None collection_task_retrieve_offset: int | None - - -@use_case_result_part -class MetricLoadMetricEntryTags(UseCaseResultBase): - """The tags associated with a metric entry.""" - - metric_entry_ref_id: EntityId - tags: list[Tag] - - -@use_case_result -class MetricLoadResult(UseCaseResultBase): - """MetricLoadResult.""" - - metric: Metric - note: Note | None - tags: list[Tag] - metric_entries: list[MetricEntry] - metric_entry_tags: list[MetricLoadMetricEntryTags] - collection_tasks: list[InboxTask] - collection_tasks_total_cnt: int - collection_tasks_page_size: int + include_entry_tags_and_contacts: bool | None = None @readonly_use_case(WorkspaceFeature.METRICS) @@ -84,108 +55,19 @@ async def _perform_transactional_read( """Execute the command's action.""" allow_archived = args.allow_archived or False allow_archived_entries = args.allow_archived_entries or False - - if ( - args.collection_task_retrieve_offset is not None - and args.collection_task_retrieve_offset < 0 - ): - raise InputValidationError("Invalid inbox_task_retrieve_offset") + include_entry_tags_and_contacts = args.include_entry_tags_and_contacts or False metric = await uow.get_for(Metric).load_by_id( args.ref_id, allow_archived=allow_archived ) - metric_entries = await uow.get_for(MetricEntry).find_all( - metric.ref_id, allow_archived=allow_archived_entries - ) - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std(NamedEntityTag.METRIC.value, metric.ref_id), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - - tags_domain = await uow.get_for(TagDomain).load_by_parent( - context.workspace.ref_id - ) - tag_links = await uow.get(TagLinkRepository).find_all_generic( - parent_ref_id=tags_domain.ref_id, - allow_archived=False, - owner=[ - EntityLink.std(NamedEntityTag.METRIC_ENTRY.value, e.ref_id) - for e in metric_entries - ], - ) - tag_links_by_entry_ref_id = { - cast(EntityId, tl.owner.ref_id): tl for tl in tag_links - } - all_entry_tag_ref_ids: list[EntityId] = [] - for tl in tag_links: - all_entry_tag_ref_ids.extend(tl.ref_ids) - if all_entry_tag_ref_ids: - all_tags = await uow.get_for(Tag).find_all_generic( - parent_ref_id=tags_domain.ref_id, - allow_archived=False, - ref_id=list(set(all_entry_tag_ref_ids)), - ) - all_tags_by_ref_id = {t.ref_id: t for t in all_tags} - else: - all_tags_by_ref_id = {} - metric_entry_tags = [ - MetricLoadMetricEntryTags( - metric_entry_ref_id=e.ref_id, - tags=( - [ - all_tags_by_ref_id[rid] - for rid in tag_links_by_entry_ref_id[e.ref_id].ref_ids - if rid in all_tags_by_ref_id - ] - if e.ref_id in tag_links_by_entry_ref_id - else [] - ), - ) - for e in metric_entries - ] - - inbox_task_collection = await uow.get_for(InboxTaskCollection).load_by_parent( - context.workspace.ref_id - ) - - collection_tasks_total_cnt = await uow.get( - InboxTaskRepository - ).count_all_for_owner( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=True, - owner=EntityLink.std(NamedEntityTag.METRIC.value, metric.ref_id), - ) - - collection_tasks = await uow.get( - InboxTaskRepository - ).find_all_for_owner_created_desc( - parent_ref_id=inbox_task_collection.ref_id, - allow_archived=True, - owner=EntityLink.std(NamedEntityTag.METRIC.value, metric.ref_id), - retrieve_offset=args.collection_task_retrieve_offset or 0, - retrieve_limit=InboxTaskRepository.PAGE_SIZE, - ) - - note = await uow.get(NoteRepository).load_optional_for_owner( - EntityLink.std(NamedEntityTag.METRIC.value, metric.ref_id), + return await MetricLoadService().do_it( + uow, + context.workspace.ref_id, + metric, allow_archived=allow_archived, - ) - - return MetricLoadResult( - metric=metric, - note=note, - tags=tags, - metric_entries=metric_entries, - metric_entry_tags=metric_entry_tags, - collection_tasks=collection_tasks, - collection_tasks_total_cnt=collection_tasks_total_cnt, - collection_tasks_page_size=InboxTaskRepository.PAGE_SIZE, + allow_archived_entries=allow_archived_entries, + include_entry_tags_and_contacts=include_entry_tags_and_contacts, + include_collection_tasks=True, + collection_task_retrieve_offset=args.collection_task_retrieve_offset, ) diff --git a/src/core/jupiter/core/metrics/use_case/load_public.py b/src/core/jupiter/core/metrics/use_case/load_public.py new file mode 100644 index 000000000..54dc6cd61 --- /dev/null +++ b/src/core/jupiter/core/metrics/use_case/load_public.py @@ -0,0 +1,87 @@ +"""Guest readonly use case for loading a published metric.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.metrics.collection import MetricCollection +from jupiter.core.metrics.root import Metric +from jupiter.core.metrics.service.load import ( + MetricLoadResult, + MetricLoadService, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class MetricLoadPublicArgs(UseCaseArgsBase): + """MetricLoadPublic args.""" + + external_id: PublishExternalId + include_entry_tags_and_contacts: bool | None + + +class MetricLoadPublicUseCase( + JupiterGuestReadonlyUseCase[MetricLoadPublicArgs, MetricLoadResult] +): + """Load a published metric by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: MetricLoadPublicArgs, + ) -> MetricLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.METRIC.value: + raise InputValidationError( + "The publish entity does not refer to a metric." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + metric_collection = await uow.get_for(MetricCollection).load_by_parent( + publish_domain.workspace.ref_id + ) + metric = await uow.get_for(Metric).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if metric.parent_ref_id != metric_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace metric." + ) + + return await MetricLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + metric, + allow_archived=False, + allow_archived_entries=False, + allow_archived_tags=False, + include_entry_tags_and_contacts=( + args.include_entry_tags_and_contacts or True + ), + include_collection_tasks=False, + include_publish_entity=False, + ) diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx index feb96c299..7e170c664 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -34,8 +34,10 @@ function publishedEntityLocation(externalId: string, owner: string): string { return `/app/public/published/smart-list/${externalId}`; case NamedEntityTag.SMART_LIST_ITEM: return `/app/public/published/smart-list/item/${externalId}`; + case NamedEntityTag.METRIC: + return `/app/public/published/metric/${externalId}`; case NamedEntityTag.METRIC_ENTRY: - return `/app/public/published/metric-entry/${externalId}`; + return `/app/public/published/metric/entry/${externalId}`; case NamedEntityTag.DOC: return `/app/public/published/doc/${externalId}`; case NamedEntityTag.PERSON: diff --git a/src/webui/app/routes/app/public/published/metric/$externalId.tsx b/src/webui/app/routes/app/public/published/metric/$externalId.tsx new file mode 100644 index 000000000..e9507c44f --- /dev/null +++ b/src/webui/app/routes/app/public/published/metric/$externalId.tsx @@ -0,0 +1,372 @@ +import type { Contact, MetricEntry, Tag } from "@jupiter/webapi-client"; +import { DocsHelpSubject, MetricDirection } from "@jupiter/webapi-client"; +import { styled } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import { ResponsiveLine } from "@nivo/line"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { Outlet } from "@remix-run/react"; +import { AnimatePresence } from "framer-motion"; +import { useContext, useMemo, useState } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { aDateToDate, compareADate } from "@jupiter/core/common/adate"; +import { metricEntryName } from "@jupiter/core/metrics/sub/entry/root"; +import { EntityNameComponent } from "@jupiter/core/common/component/entity-name"; +import { EntityNoNothingCard } from "@jupiter/core/infra/component/entity-no-nothing-card"; +import { + EntityCard, + EntityLink, +} from "@jupiter/core/infra/component/entity-card"; +import { EntityStack } from "@jupiter/core/infra/component/entity-stack"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { NestingAwareBlock } from "@jupiter/core/infra/component/layout/nesting-aware-block"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { TimeDiffTag } from "@jupiter/core/common/component/time-diff-tag"; +import { + DisplayType, + useLeafNeedsToShowLeaflet, +} from "@jupiter/core/infra/component/use-nested-entities"; +import { + FilterManyOptions, + SectionActions, +} from "@jupiter/core/infra/component/section-actions"; +import { TagTag } from "@jupiter/core/common/sub/tags/component/tag-tag"; +import { ContactTag } from "@jupiter/core/common/sub/contacts/component/contact-tag"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + const response = await apiClient.metrics.metricLoadPublic({ + external_id: externalId, + include_entry_tags_and_contacts: true, + }); + + const metricEntryContactsByRefId: { [key: string]: Array<Contact> } = + response.metric_entry_contacts ?? {}; + + const tagsByMetricEntryRefId: { [key: string]: Array<Tag> } = {}; + for (const entryTags of response.metric_entry_tags) { + tagsByMetricEntryRefId[entryTags.metric_entry_ref_id] = entryTags.tags; + } + + const allEntryTags = Object.values(tagsByMetricEntryRefId) + .flat() + .filter( + (tag, index, tags) => + tags.findIndex((other) => other.ref_id === tag.ref_id) === index, + ); + const allContacts = Object.values(metricEntryContactsByRefId) + .flat() + .filter( + (contact, index, contacts) => + contacts.findIndex((other) => other.ref_id === contact.ref_id) === + index, + ); + + return json({ + externalId, + metric: response.metric, + metricEntries: response.metric_entries, + tagsByMetricEntryRefId, + metricEntryContactsByRefId, + allEntryTags, + allContacts, + }); + } catch (error) { + handlePublishedLoaderError(error); + } +} + +export default function PublishedMetric() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const shouldShowALeaflet = useLeafNeedsToShowLeaflet(); + + const [selectedTagsRefId, setSelectedTagsRefId] = useState<string[]>([]); + const [selectedContactsRefId, setSelectedContactsRefId] = useState<string[]>( + [], + ); + + const allEntriesSorted = useMemo( + () => + [...loaderData.metricEntries].sort( + (e1, e2) => -compareADate(e1.collection_time, e2.collection_time), + ), + [loaderData.metricEntries], + ); + + const previousEntryByRefId = useMemo(() => { + const map = new Map<string, MetricEntry>(); + for (let i = 0; i < allEntriesSorted.length - 1; i++) { + map.set(allEntriesSorted[i].ref_id, allEntriesSorted[i + 1]); + } + return map; + }, [allEntriesSorted]); + + const filteredEntries = useMemo( + () => + allEntriesSorted.filter((entry) => { + const tags = loaderData.tagsByMetricEntryRefId[entry.ref_id] ?? []; + const tagsOk = + selectedTagsRefId.length === 0 || + tags.some((tag) => selectedTagsRefId.includes(tag.ref_id)); + + const contacts = + loaderData.metricEntryContactsByRefId[entry.ref_id] ?? []; + const contactsOk = + selectedContactsRefId.length === 0 || + contacts.some((contact: Contact) => + selectedContactsRefId.includes(contact.ref_id), + ); + + return tagsOk && contactsOk; + }), + [ + allEntriesSorted, + loaderData.tagsByMetricEntryRefId, + loaderData.metricEntryContactsByRefId, + selectedTagsRefId, + selectedContactsRefId, + ], + ); + + function getDirectionIndicator( + entry: MetricEntry, + ): { arrow: string; diff: string; color: string } | null { + const direction = loaderData.metric.metric_direction; + if (direction === MetricDirection.NONE) return null; + + const prev = previousEntryByRefId.get(entry.ref_id); + if (!prev) return null; + + const delta = entry.value - prev.value; + const roundedDelta = Math.round(delta * 100) / 100; + if (roundedDelta === 0) return null; + + const isUp = roundedDelta > 0; + const diffStr = isUp + ? `+${roundedDelta.toFixed(2)}` + : `${roundedDelta.toFixed(2)}`; + + const isGood = + (direction === MetricDirection.UP_IS_GOOD && isUp) || + (direction === MetricDirection.DOWN_IS_GOOD && !isUp); + + return { + arrow: isUp ? "⬆" : "⬇", + diff: diffStr, + color: isGood ? "green" : "red", + }; + } + + return ( + <LeafPanel + key={`published-metric-${loaderData.metric.ref_id}`} + fakeKey={`published-metric-${loaderData.metric.ref_id}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + shouldShowALeaflet={shouldShowALeaflet} + > + <NestingAwareBlock shouldHide={shouldShowALeaflet}> + <SectionCard + title={loaderData.metric.name} + actions={ + <SectionActions + id="published-metric-entries" + topLevelInfo={topLevelInfo} + inputsEnabled={false} + actions={[ + FilterManyOptions( + "Tags", + loaderData.allEntryTags.map((tag) => ({ + value: tag.ref_id, + text: tag.name, + })), + setSelectedTagsRefId, + ), + FilterManyOptions( + "Contacts", + loaderData.allContacts.map((contact) => ({ + value: contact.ref_id, + text: contact.name, + })), + setSelectedContactsRefId, + ), + ]} + /> + } + > + <MetricGraph sortedMetricEntries={filteredEntries} /> + + {filteredEntries.length === 0 && ( + <EntityNoNothingCard + title="Nothing To Show" + message="There are no metric entries to show with the current filters." + helpSubject={DocsHelpSubject.METRICS} + /> + )} + + <EntityStack> + {filteredEntries.map((entry) => { + const indicator = getDirectionIndicator(entry); + return ( + <EntityCard + key={`metric-entry-${entry.ref_id}`} + entityId={`metric-entry-${entry.ref_id}`} + > + <EntityLink + to={`/app/public/published/metric/${loaderData.externalId}/${entry.ref_id}`} + > + <EntityNameComponent name={metricEntryName(entry)} /> + {indicator && ( + <span + style={{ + color: indicator.color, + fontWeight: "bold", + fontSize: "0.9em", + marginLeft: "4px", + }} + > + {indicator.arrow} {indicator.diff} + </span> + )} + <TimeDiffTag + today={topLevelInfo.today} + labelPrefix="Collected" + collectionTime={entry.collection_time} + /> + {( + loaderData.tagsByMetricEntryRefId[entry.ref_id] ?? [] + ).map((tag) => ( + <TagTag key={tag.ref_id} tag={tag} /> + ))} + {( + loaderData.metricEntryContactsByRefId[entry.ref_id] ?? [] + ).map((contact: Contact) => ( + <ContactTag key={contact.ref_id} contact={contact} /> + ))} + </EntityLink> + </EntityCard> + ); + })} + </EntityStack> + </SectionCard> + </NestingAwareBlock> + + <AnimatePresence mode="wait" initial={false}> + <Outlet /> + </AnimatePresence> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => `Could not find published metric ${params.externalId}!`, + error: (params) => + `There was an error loading published metric ${params.externalId}! Please try again!`, +}); + +interface MetricGraphProps { + sortedMetricEntries: MetricEntry[]; +} + +function MetricGraph({ sortedMetricEntries }: MetricGraphProps) { + const theme = useTheme(); + const nivoTheme = { + axis: { + ticks: { + text: { fill: theme.palette.text.secondary }, + }, + legend: { + text: { fill: theme.palette.text.primary }, + }, + }, + legends: { + text: { fill: theme.palette.text.primary }, + }, + tooltip: { + container: { + background: theme.palette.background.paper, + color: theme.palette.text.primary, + }, + }, + }; + + if (sortedMetricEntries.length === 0) { + return null; + } + + const entriesForGraph = sortedMetricEntries.map((e) => ({ + x: aDateToDate(e.collection_time).toFormat("yyyy-MM-dd"), + y: e.value, + refId: e.ref_id, + })); + const graphMaxValue = Math.max(...entriesForGraph.map((e) => e.y)) * 1.35; + + return ( + <MetricGraphDiv> + <ResponsiveLine + theme={nivoTheme} + curve="monotoneX" + xScale={{ + type: "time", + format: "%Y-%m-%d", + useUTC: false, + precision: "day", + }} + xFormat="time:%Y-%m-%d" + yScale={{ + type: "linear", + nice: true, + min: 0, + max: graphMaxValue, + }} + axisBottom={{ + format: "'%y-%b-%d", + tickValues: 7, + }} + pointSize={4} + pointBorderWidth={1} + pointBorderColor={{ + from: "color", + modifiers: [["darker", 0.3]], + }} + margin={{ top: 20, right: 10, bottom: 50, left: 50 }} + useMesh={true} + enableSlices={false} + data={[ + { + id: "metricValue", + data: entriesForGraph, + }, + ]} + /> + </MetricGraphDiv> + ); +} + +const MetricGraphDiv = styled("div")` + height: 300px; +`; diff --git a/src/webui/app/routes/app/public/published/metric/$externalId/$entryId.tsx b/src/webui/app/routes/app/public/published/metric/$externalId/$entryId.tsx new file mode 100644 index 000000000..ab5fe6807 --- /dev/null +++ b/src/webui/app/routes/app/public/published/metric/$externalId/$entryId.tsx @@ -0,0 +1,96 @@ +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { useContext } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { MetricEntryEditor } from "@jupiter/core/metrics/sub/entry/component/editor"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), + entryId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAFLET, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + const { externalId, entryId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + const result = await apiClient.metrics.metricEntryLoadPublicFromMetric({ + external_id: externalId, + ref_id: entryId, + }); + + return json({ + externalId, + metricEntry: result.metric_entry, + tags: result.tags ?? [], + contacts: result.contacts ?? [], + note: result.note ?? null, + }); + } catch (error) { + handlePublishedLoaderError(error); + } +} + +export default function PublishedMetricEntryFromMetric() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const { metricEntry, tags, contacts, note } = loaderData; + + return ( + <LeafPanel + key={`published-metric-entry-${metricEntry.ref_id}`} + fakeKey={`published-metric-entry-${metricEntry.ref_id}`} + isLeaflet + inputsEnabled={false} + entityNotEditable={true} + returnLocation={`/app/public/published/metric/${loaderData.externalId}`} + > + <MetricEntryEditor + metricEntry={metricEntry} + tags={tags} + contacts={contacts} + allTags={tags} + allContacts={contacts} + inputsEnabled={false} + topLevelInfo={topLevelInfo} + /> + + <SectionCard title="Note"> + {note ? ( + <EntityNoteEditor initialNote={note} inputsEnabled={false} /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary( + (params) => `/app/public/published/metric/${params.externalId}`, + ParamsSchema, + { + notFound: (params) => + `Could not find metric entry ${params.entryId} in published metric ${params.externalId}!`, + error: (params) => + `There was an error loading metric entry ${params.entryId}! Please try again!`, + }, +); diff --git a/src/webui/app/routes/app/public/published/metric-entry/$externalId.tsx b/src/webui/app/routes/app/public/published/metric/entry/$externalId.tsx similarity index 100% rename from src/webui/app/routes/app/public/published/metric-entry/$externalId.tsx rename to src/webui/app/routes/app/public/published/metric/entry/$externalId.tsx diff --git a/src/webui/app/routes/app/workspace/metrics/$id.tsx b/src/webui/app/routes/app/workspace/metrics/$id.tsx index 5779ae1ff..efcbcdeb8 100644 --- a/src/webui/app/routes/app/workspace/metrics/$id.tsx +++ b/src/webui/app/routes/app/workspace/metrics/$id.tsx @@ -60,6 +60,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -75,6 +87,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ref_id: id, allow_archived: true, allow_archived_entries: false, + include_entry_tags_and_contacts: true, }); const allTags = await apiClient.tags.tagFind({ @@ -84,20 +97,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { allow_archived: false, }); - const metricEntryContactPairs = await Promise.all( - response.metric_entries.map(async (entry) => { - const entryLoadResult = await apiClient.metrics.metricEntryLoad({ - ref_id: entry.ref_id, - allow_archived: true, - }); - return [ - entry.ref_id, - entryLoadResult.contacts as Array<Contact>, - ] as const; - }), - ); const metricEntryContactsByRefId: { [key: string]: Array<Contact> } = - Object.fromEntries(metricEntryContactPairs); + response.metric_entry_contacts ?? {}; return json({ metric: response.metric, @@ -106,6 +107,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { allTags: allTags.tags, allContacts: allContacts.contacts as Array<Contact>, metricEntryContactsByRefId, + publishEntity: response.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -142,6 +144,30 @@ export async function action({ request, params }: LoaderFunctionArgs) { return redirect(`/app/workspace/metrics`); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect(`/app/workspace/metrics/${id}`); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/metrics/${id}`); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect(`/app/workspace/metrics/${id}`); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -242,6 +268,8 @@ export default function Metric() { entityRefId={loaderData.metric.ref_id} createLocation={`/app/workspace/metrics/${loaderData.metric.ref_id}/entries/new`} returnLocation="/app/workspace/metrics" + publishable + publishEntity={loaderData.publishEntity ?? undefined} actions={ <SectionActions id={`metric-${loaderData.metric.ref_id}-actions`} From 07d4249c3acf5d4eca264ae0725d259f7313c51a Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Mon, 8 Jun 2026 20:15:53 +0300 Subject: [PATCH 12/21] Add for calendars --- ...alendar_load_public_for_schedule_stream.py | 216 +++++ .../metric_entry_load_public_from_metric.py | 20 +- .../api/metrics/metric_load_public.py | 8 +- ...l_days_load_public_from_schedule_stream.py | 218 +++++ ...in_day_load_public_from_schedule_stream.py | 218 +++++ .../schedule/schedule_stream_load_public.py | 212 +++++ .../jupiter_webapi_client/models/__init__.py | 20 +- ...ar_load_public_for_schedule_stream_args.py | 111 +++ .../models/metric_load_args.py | 38 +- .../models/metric_load_public_args.py | 4 +- .../models/metric_load_result.py | 66 ++ ...oad_result_metric_entry_contacts_type_0.py | 68 ++ ...s_load_public_from_schedule_stream_args.py | 70 ++ ...y_load_public_from_schedule_stream_args.py | 70 ++ .../schedule_stream_load_public_args.py | 62 ++ .../models/schedule_stream_load_result.py | 35 +- gen/ts/webapi-client/gen/index.ts | 6 +- ...CalendarLoadPublicForScheduleStreamArgs.ts | 17 + .../MetricEntryLoadPublicFromMetricArgs.ts | 1 + .../gen/models/MetricLoadArgs.ts | 2 +- .../gen/models/MetricLoadPublicArgs.ts | 1 + ...ullDaysLoadPublicFromScheduleStreamArgs.ts | 14 + ...ntInDayLoadPublicFromScheduleStreamArgs.ts | 14 + .../models/ScheduleStreamLoadPublicArgs.ts | 12 + .../gen/models/ScheduleStreamLoadResult.ts | 4 +- .../gen/services/CalendarService.ts | 29 + .../gen/services/MetricsService.ts | 16 +- .../gen/services/ScheduleService.ts | 87 ++ itests/webui/entities/schedules.test.py | 47 + .../component/calendar-navigation.tsx | 135 +++ .../core/calendar/component/shared.tsx | 156 ++-- .../jupiter/core/calendar/service/__init__.py | 1 + .../service/load_for_date_and_period.py | 856 ++++++++++++++++++ .../use_case/load_for_date_and_period.py | 648 +------------ .../load_public_for_schedule_stream.py | 113 +++ .../common/sub/publish/sub/entity/root.py | 2 +- .../load_public_from_schedule_stream.py | 104 +++ .../load_public_from_schedule_stream.py | 99 ++ .../jupiter/core/schedule/sub/stream/root.py | 4 + .../schedule/sub/stream/service/__init__.py | 1 + .../core/schedule/sub/stream/service/load.py | 81 ++ .../core/schedule/sub/stream/use_case/load.py | 52 +- .../sub/stream/use_case/load_public.py | 79 ++ .../app/public/published/$externalId.tsx | 2 + .../published/schedule-stream/$externalId.tsx | 310 +++++++ .../$externalId/full-days-event/$eventId.tsx | 111 +++ .../$externalId/in-day-event/$eventId.tsx | 125 +++ .../calendar/schedule/stream/$id.tsx | 45 + 48 files changed, 3827 insertions(+), 783 deletions(-) create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/calendar/calendar_load_public_for_schedule_stream.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_full_days_load_public_from_schedule_stream.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_in_day_load_public_from_schedule_stream.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_stream_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/calendar_load_public_for_schedule_stream_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/metric_load_result_metric_entry_contacts_type_0.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_public_from_schedule_stream_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_public_from_schedule_stream_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/schedule_stream_load_public_args.py create mode 100644 gen/ts/webapi-client/gen/models/CalendarLoadPublicForScheduleStreamArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadPublicFromScheduleStreamArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/ScheduleStreamLoadPublicArgs.ts create mode 100644 src/core/jupiter/core/calendar/component/calendar-navigation.tsx create mode 100644 src/core/jupiter/core/calendar/service/__init__.py create mode 100644 src/core/jupiter/core/calendar/service/load_for_date_and_period.py create mode 100644 src/core/jupiter/core/calendar/use_case/load_public_for_schedule_stream.py create mode 100644 src/core/jupiter/core/schedule/sub/event_full_days/use_case/load_public_from_schedule_stream.py create mode 100644 src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public_from_schedule_stream.py create mode 100644 src/core/jupiter/core/schedule/sub/stream/service/__init__.py create mode 100644 src/core/jupiter/core/schedule/sub/stream/service/load.py create mode 100644 src/core/jupiter/core/schedule/sub/stream/use_case/load_public.py create mode 100644 src/webui/app/routes/app/public/published/schedule-stream/$externalId.tsx create mode 100644 src/webui/app/routes/app/public/published/schedule-stream/$externalId/full-days-event/$eventId.tsx create mode 100644 src/webui/app/routes/app/public/published/schedule-stream/$externalId/in-day-event/$eventId.tsx diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/calendar/calendar_load_public_for_schedule_stream.py b/gen/py/webapi-client/jupiter_webapi_client/api/calendar/calendar_load_public_for_schedule_stream.py new file mode 100644 index 000000000..538f6c2f5 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/calendar/calendar_load_public_for_schedule_stream.py @@ -0,0 +1,216 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.calendar_load_for_date_and_period_result import CalendarLoadForDateAndPeriodResult +from ...models.calendar_load_public_for_schedule_stream_args import CalendarLoadPublicForScheduleStreamArgs +from ...models.error_response import ErrorResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: CalendarLoadPublicForScheduleStreamArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/calendar-load-public-for-schedule-stream", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> CalendarLoadForDateAndPeriodResult | ErrorResponse | None: + if response.status_code == 200: + response_200 = CalendarLoadForDateAndPeriodResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[CalendarLoadForDateAndPeriodResult | ErrorResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: CalendarLoadPublicForScheduleStreamArgs | Unset = UNSET, +) -> Response[CalendarLoadForDateAndPeriodResult | ErrorResponse]: + """Load calendar entries and stats for a published schedule stream. + + Args: + body (CalendarLoadPublicForScheduleStreamArgs | Unset): + CalendarLoadPublicForScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[CalendarLoadForDateAndPeriodResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: CalendarLoadPublicForScheduleStreamArgs | Unset = UNSET, +) -> CalendarLoadForDateAndPeriodResult | ErrorResponse | None: + """Load calendar entries and stats for a published schedule stream. + + Args: + body (CalendarLoadPublicForScheduleStreamArgs | Unset): + CalendarLoadPublicForScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + CalendarLoadForDateAndPeriodResult | ErrorResponse + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: CalendarLoadPublicForScheduleStreamArgs | Unset = UNSET, +) -> Response[CalendarLoadForDateAndPeriodResult | ErrorResponse]: + """Load calendar entries and stats for a published schedule stream. + + Args: + body (CalendarLoadPublicForScheduleStreamArgs | Unset): + CalendarLoadPublicForScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[CalendarLoadForDateAndPeriodResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: CalendarLoadPublicForScheduleStreamArgs | Unset = UNSET, +) -> CalendarLoadForDateAndPeriodResult | ErrorResponse | None: + """Load calendar entries and stats for a published schedule stream. + + Args: + body (CalendarLoadPublicForScheduleStreamArgs | Unset): + CalendarLoadPublicForScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + CalendarLoadForDateAndPeriodResult | ErrorResponse + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public_from_metric.py b/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public_from_metric.py index fb56d56dc..fd51ec782 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public_from_metric.py +++ b/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_entry_load_public_from_metric.py @@ -111,11 +111,10 @@ def sync_detailed( client: AuthenticatedClient | Client, body: MetricEntryLoadPublicFromMetricArgs | Unset = UNSET, ) -> Response[ErrorResponse | MetricEntryLoadResult]: - """Load a smart list item through a published smart list. + """Load a metric entry through a published metric. Args: - body (MetricEntryLoadPublicFromMetricArgs | Unset): - MetricEntryLoadPublicFromMetric args. + body (MetricEntryLoadPublicFromMetricArgs | Unset): MetricEntryLoadPublicFromMetric args. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -141,11 +140,10 @@ def sync( client: AuthenticatedClient | Client, body: MetricEntryLoadPublicFromMetricArgs | Unset = UNSET, ) -> ErrorResponse | MetricEntryLoadResult | None: - """Load a smart list item through a published smart list. + """Load a metric entry through a published metric. Args: - body (MetricEntryLoadPublicFromMetricArgs | Unset): - MetricEntryLoadPublicFromMetric args. + body (MetricEntryLoadPublicFromMetricArgs | Unset): MetricEntryLoadPublicFromMetric args. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -166,11 +164,10 @@ async def asyncio_detailed( client: AuthenticatedClient | Client, body: MetricEntryLoadPublicFromMetricArgs | Unset = UNSET, ) -> Response[ErrorResponse | MetricEntryLoadResult]: - """Load a smart list item through a published smart list. + """Load a metric entry through a published metric. Args: - body (MetricEntryLoadPublicFromMetricArgs | Unset): - MetricEntryLoadPublicFromMetric args. + body (MetricEntryLoadPublicFromMetricArgs | Unset): MetricEntryLoadPublicFromMetric args. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. @@ -194,11 +191,10 @@ async def asyncio( client: AuthenticatedClient | Client, body: MetricEntryLoadPublicFromMetricArgs | Unset = UNSET, ) -> ErrorResponse | MetricEntryLoadResult | None: - """Load a smart list item through a published smart list. + """Load a metric entry through a published metric. Args: - body (MetricEntryLoadPublicFromMetricArgs | Unset): - MetricEntryLoadPublicFromMetric args. + body (MetricEntryLoadPublicFromMetricArgs | Unset): MetricEntryLoadPublicFromMetric args. Raises: errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_load_public.py index 10c99483b..f1a36cb2f 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_load_public.py +++ b/gen/py/webapi-client/jupiter_webapi_client/api/metrics/metric_load_public.py @@ -111,7 +111,7 @@ def sync_detailed( client: AuthenticatedClient | Client, body: MetricLoadPublicArgs | Unset = UNSET, ) -> Response[ErrorResponse | MetricLoadResult]: - """Load a published smart list by publish external id. + """Load a published metric by publish external id. Args: body (MetricLoadPublicArgs | Unset): MetricLoadPublic args. @@ -140,7 +140,7 @@ def sync( client: AuthenticatedClient | Client, body: MetricLoadPublicArgs | Unset = UNSET, ) -> ErrorResponse | MetricLoadResult | None: - """Load a published smart list by publish external id. + """Load a published metric by publish external id. Args: body (MetricLoadPublicArgs | Unset): MetricLoadPublic args. @@ -164,7 +164,7 @@ async def asyncio_detailed( client: AuthenticatedClient | Client, body: MetricLoadPublicArgs | Unset = UNSET, ) -> Response[ErrorResponse | MetricLoadResult]: - """Load a published smart list by publish external id. + """Load a published metric by publish external id. Args: body (MetricLoadPublicArgs | Unset): MetricLoadPublic args. @@ -191,7 +191,7 @@ async def asyncio( client: AuthenticatedClient | Client, body: MetricLoadPublicArgs | Unset = UNSET, ) -> ErrorResponse | MetricLoadResult | None: - """Load a published smart list by publish external id. + """Load a published metric by publish external id. Args: body (MetricLoadPublicArgs | Unset): MetricLoadPublic args. diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_full_days_load_public_from_schedule_stream.py b/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_full_days_load_public_from_schedule_stream.py new file mode 100644 index 000000000..f85d1a21c --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_full_days_load_public_from_schedule_stream.py @@ -0,0 +1,218 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.schedule_event_full_days_load_public_from_schedule_stream_args import ( + ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs, +) +from ...models.schedule_event_full_days_load_result import ScheduleEventFullDaysLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/schedule-event-full-days-load-public-from-schedule-stream", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | ScheduleEventFullDaysLoadResult | None: + if response.status_code == 200: + response_200 = ScheduleEventFullDaysLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | ScheduleEventFullDaysLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs | Unset = UNSET, +) -> Response[ErrorResponse | ScheduleEventFullDaysLoadResult]: + """Load a schedule full days event through a published schedule stream. + + Args: + body (ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs | Unset): + ScheduleEventFullDaysLoadPublicFromScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | ScheduleEventFullDaysLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs | Unset = UNSET, +) -> ErrorResponse | ScheduleEventFullDaysLoadResult | None: + """Load a schedule full days event through a published schedule stream. + + Args: + body (ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs | Unset): + ScheduleEventFullDaysLoadPublicFromScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | ScheduleEventFullDaysLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs | Unset = UNSET, +) -> Response[ErrorResponse | ScheduleEventFullDaysLoadResult]: + """Load a schedule full days event through a published schedule stream. + + Args: + body (ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs | Unset): + ScheduleEventFullDaysLoadPublicFromScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | ScheduleEventFullDaysLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs | Unset = UNSET, +) -> ErrorResponse | ScheduleEventFullDaysLoadResult | None: + """Load a schedule full days event through a published schedule stream. + + Args: + body (ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs | Unset): + ScheduleEventFullDaysLoadPublicFromScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | ScheduleEventFullDaysLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_in_day_load_public_from_schedule_stream.py b/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_in_day_load_public_from_schedule_stream.py new file mode 100644 index 000000000..48fcb0588 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_event_in_day_load_public_from_schedule_stream.py @@ -0,0 +1,218 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.schedule_event_in_day_load_public_from_schedule_stream_args import ( + ScheduleEventInDayLoadPublicFromScheduleStreamArgs, +) +from ...models.schedule_event_in_day_load_result import ScheduleEventInDayLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: ScheduleEventInDayLoadPublicFromScheduleStreamArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/schedule-event-in-day-load-public-from-schedule-stream", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | ScheduleEventInDayLoadResult | None: + if response.status_code == 200: + response_200 = ScheduleEventInDayLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | ScheduleEventInDayLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventInDayLoadPublicFromScheduleStreamArgs | Unset = UNSET, +) -> Response[ErrorResponse | ScheduleEventInDayLoadResult]: + """Load a schedule event in day through a published schedule stream. + + Args: + body (ScheduleEventInDayLoadPublicFromScheduleStreamArgs | Unset): + ScheduleEventInDayLoadPublicFromScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | ScheduleEventInDayLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventInDayLoadPublicFromScheduleStreamArgs | Unset = UNSET, +) -> ErrorResponse | ScheduleEventInDayLoadResult | None: + """Load a schedule event in day through a published schedule stream. + + Args: + body (ScheduleEventInDayLoadPublicFromScheduleStreamArgs | Unset): + ScheduleEventInDayLoadPublicFromScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | ScheduleEventInDayLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventInDayLoadPublicFromScheduleStreamArgs | Unset = UNSET, +) -> Response[ErrorResponse | ScheduleEventInDayLoadResult]: + """Load a schedule event in day through a published schedule stream. + + Args: + body (ScheduleEventInDayLoadPublicFromScheduleStreamArgs | Unset): + ScheduleEventInDayLoadPublicFromScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | ScheduleEventInDayLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: ScheduleEventInDayLoadPublicFromScheduleStreamArgs | Unset = UNSET, +) -> ErrorResponse | ScheduleEventInDayLoadResult | None: + """Load a schedule event in day through a published schedule stream. + + Args: + body (ScheduleEventInDayLoadPublicFromScheduleStreamArgs | Unset): + ScheduleEventInDayLoadPublicFromScheduleStream args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | ScheduleEventInDayLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_stream_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_stream_load_public.py new file mode 100644 index 000000000..c611b806b --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/schedule/schedule_stream_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.schedule_stream_load_public_args import ScheduleStreamLoadPublicArgs +from ...models.schedule_stream_load_result import ScheduleStreamLoadResult +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: ScheduleStreamLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/schedule-stream-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | ScheduleStreamLoadResult | None: + if response.status_code == 200: + response_200 = ScheduleStreamLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | ScheduleStreamLoadResult]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: ScheduleStreamLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | ScheduleStreamLoadResult]: + """Load a published schedule stream by publish external id. + + Args: + body (ScheduleStreamLoadPublicArgs | Unset): ScheduleStreamLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | ScheduleStreamLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: ScheduleStreamLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | ScheduleStreamLoadResult | None: + """Load a published schedule stream by publish external id. + + Args: + body (ScheduleStreamLoadPublicArgs | Unset): ScheduleStreamLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | ScheduleStreamLoadResult + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: ScheduleStreamLoadPublicArgs | Unset = UNSET, +) -> Response[ErrorResponse | ScheduleStreamLoadResult]: + """Load a published schedule stream by publish external id. + + Args: + body (ScheduleStreamLoadPublicArgs | Unset): ScheduleStreamLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | ScheduleStreamLoadResult] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: ScheduleStreamLoadPublicArgs | Unset = UNSET, +) -> ErrorResponse | ScheduleStreamLoadResult | None: + """Load a published schedule stream by publish external id. + + Args: + body (ScheduleStreamLoadPublicArgs | Unset): ScheduleStreamLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | ScheduleStreamLoadResult + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index 9d00b5867..3b3bfce49 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -104,6 +104,7 @@ from .calendar_events_stats_per_subperiod import CalendarEventsStatsPerSubperiod from .calendar_load_for_date_and_period_args import CalendarLoadForDateAndPeriodArgs from .calendar_load_for_date_and_period_result import CalendarLoadForDateAndPeriodResult +from .calendar_load_public_for_schedule_stream_args import CalendarLoadPublicForScheduleStreamArgs from .change_password_args import ChangePasswordArgs from .chapter import Chapter from .chapter_archive_args import ChapterArchiveArgs @@ -500,6 +501,7 @@ from .metric_entry_create_result import MetricEntryCreateResult from .metric_entry_load_args import MetricEntryLoadArgs from .metric_entry_load_public_args import MetricEntryLoadPublicArgs +from .metric_entry_load_public_from_metric_args import MetricEntryLoadPublicFromMetricArgs from .metric_entry_load_result import MetricEntryLoadResult from .metric_entry_remove_args import MetricEntryRemoveArgs from .metric_entry_update_args import MetricEntryUpdateArgs @@ -508,11 +510,11 @@ from .metric_find_args import MetricFindArgs from .metric_find_response_entry import MetricFindResponseEntry from .metric_find_result import MetricFindResult -from .metric_entry_load_public_from_metric_args import MetricEntryLoadPublicFromMetricArgs from .metric_load_args import MetricLoadArgs from .metric_load_metric_entry_tags import MetricLoadMetricEntryTags from .metric_load_public_args import MetricLoadPublicArgs from .metric_load_result import MetricLoadResult +from .metric_load_result_metric_entry_contacts_type_0 import MetricLoadResultMetricEntryContactsType0 from .metric_load_settings_args import MetricLoadSettingsArgs from .metric_load_settings_result import MetricLoadSettingsResult from .metric_regen_args import MetricRegenArgs @@ -666,6 +668,9 @@ from .schedule_event_full_days_create_result import ScheduleEventFullDaysCreateResult from .schedule_event_full_days_load_args import ScheduleEventFullDaysLoadArgs from .schedule_event_full_days_load_public_args import ScheduleEventFullDaysLoadPublicArgs +from .schedule_event_full_days_load_public_from_schedule_stream_args import ( + ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs, +) from .schedule_event_full_days_load_result import ScheduleEventFullDaysLoadResult from .schedule_event_full_days_remove_args import ScheduleEventFullDaysRemoveArgs from .schedule_event_full_days_update_args import ScheduleEventFullDaysUpdateArgs @@ -679,6 +684,9 @@ from .schedule_event_in_day_create_result import ScheduleEventInDayCreateResult from .schedule_event_in_day_load_args import ScheduleEventInDayLoadArgs from .schedule_event_in_day_load_public_args import ScheduleEventInDayLoadPublicArgs +from .schedule_event_in_day_load_public_from_schedule_stream_args import ( + ScheduleEventInDayLoadPublicFromScheduleStreamArgs, +) from .schedule_event_in_day_load_result import ScheduleEventInDayLoadResult from .schedule_event_in_day_remove_args import ScheduleEventInDayRemoveArgs from .schedule_event_in_day_update_args import ScheduleEventInDayUpdateArgs @@ -722,6 +730,7 @@ from .schedule_stream_find_result import ScheduleStreamFindResult from .schedule_stream_find_result_entry import ScheduleStreamFindResultEntry from .schedule_stream_load_args import ScheduleStreamLoadArgs +from .schedule_stream_load_public_args import ScheduleStreamLoadPublicArgs from .schedule_stream_load_result import ScheduleStreamLoadResult from .schedule_stream_remove_args import ScheduleStreamRemoveArgs from .schedule_stream_source import ScheduleStreamSource @@ -1132,6 +1141,7 @@ "CalendarEventsStatsPerSubperiod", "CalendarLoadForDateAndPeriodArgs", "CalendarLoadForDateAndPeriodResult", + "CalendarLoadPublicForScheduleStreamArgs", "ChangePasswordArgs", "Chapter", "ChapterArchiveArgs", @@ -1518,6 +1528,7 @@ "MetricEntryCreateResult", "MetricEntryLoadArgs", "MetricEntryLoadPublicArgs", + "MetricEntryLoadPublicFromMetricArgs", "MetricEntryLoadResult", "MetricEntryRemoveArgs", "MetricEntryUpdateArgs", @@ -1526,11 +1537,11 @@ "MetricFindArgs", "MetricFindResponseEntry", "MetricFindResult", - "MetricEntryLoadPublicFromMetricArgs", "MetricLoadArgs", - "MetricLoadPublicArgs", "MetricLoadMetricEntryTags", + "MetricLoadPublicArgs", "MetricLoadResult", + "MetricLoadResultMetricEntryContactsType0", "MetricLoadSettingsArgs", "MetricLoadSettingsResult", "MetricRegenArgs", @@ -1682,6 +1693,7 @@ "ScheduleEventFullDaysCreateResult", "ScheduleEventFullDaysLoadArgs", "ScheduleEventFullDaysLoadPublicArgs", + "ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs", "ScheduleEventFullDaysLoadResult", "ScheduleEventFullDaysRemoveArgs", "ScheduleEventFullDaysUpdateArgs", @@ -1695,6 +1707,7 @@ "ScheduleEventInDayCreateResult", "ScheduleEventInDayLoadArgs", "ScheduleEventInDayLoadPublicArgs", + "ScheduleEventInDayLoadPublicFromScheduleStreamArgs", "ScheduleEventInDayLoadResult", "ScheduleEventInDayRemoveArgs", "ScheduleEventInDayUpdateArgs", @@ -1738,6 +1751,7 @@ "ScheduleStreamFindResult", "ScheduleStreamFindResultEntry", "ScheduleStreamLoadArgs", + "ScheduleStreamLoadPublicArgs", "ScheduleStreamLoadResult", "ScheduleStreamRemoveArgs", "ScheduleStreamSource", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/calendar_load_public_for_schedule_stream_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/calendar_load_public_for_schedule_stream_args.py new file mode 100644 index 000000000..4267c3545 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/calendar_load_public_for_schedule_stream_args.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.recurring_task_period import RecurringTaskPeriod +from ..types import UNSET, Unset + +T = TypeVar("T", bound="CalendarLoadPublicForScheduleStreamArgs") + + +@_attrs_define +class CalendarLoadPublicForScheduleStreamArgs: + """CalendarLoadPublicForScheduleStream args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + right_now (str): A date or possibly a datetime for the application. + period (RecurringTaskPeriod): A period for a particular task. + stats_subperiod (None | RecurringTaskPeriod | Unset): + """ + + external_id: str + right_now: str + period: RecurringTaskPeriod + stats_subperiod: None | RecurringTaskPeriod | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + right_now = self.right_now + + period = self.period.value + + stats_subperiod: None | str | Unset + if isinstance(self.stats_subperiod, Unset): + stats_subperiod = UNSET + elif isinstance(self.stats_subperiod, RecurringTaskPeriod): + stats_subperiod = self.stats_subperiod.value + else: + stats_subperiod = self.stats_subperiod + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + "right_now": right_now, + "period": period, + } + ) + if stats_subperiod is not UNSET: + field_dict["stats_subperiod"] = stats_subperiod + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + right_now = d.pop("right_now") + + period = RecurringTaskPeriod(d.pop("period")) + + def _parse_stats_subperiod(data: object) -> None | RecurringTaskPeriod | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + stats_subperiod_type_0 = RecurringTaskPeriod(data) + + return stats_subperiod_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | RecurringTaskPeriod | Unset, data) + + stats_subperiod = _parse_stats_subperiod(d.pop("stats_subperiod", UNSET)) + + calendar_load_public_for_schedule_stream_args = cls( + external_id=external_id, + right_now=right_now, + period=period, + stats_subperiod=stats_subperiod, + ) + + calendar_load_public_for_schedule_stream_args.additional_properties = d + return calendar_load_public_for_schedule_stream_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_args.py index 1528fef1f..269d8a47f 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_args.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_args.py @@ -19,15 +19,15 @@ class MetricLoadArgs: ref_id (str): A generic entity id. allow_archived (bool | None | Unset): allow_archived_entries (bool | None | Unset): - include_entry_tags_and_contacts (bool | None | Unset): collection_task_retrieve_offset (int | None | Unset): + include_entry_tags_and_contacts (bool | None | Unset): """ ref_id: str allow_archived: bool | None | Unset = UNSET allow_archived_entries: bool | None | Unset = UNSET - include_entry_tags_and_contacts: bool | None | Unset = UNSET collection_task_retrieve_offset: int | None | Unset = UNSET + include_entry_tags_and_contacts: bool | None | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -45,18 +45,18 @@ def to_dict(self) -> dict[str, Any]: else: allow_archived_entries = self.allow_archived_entries - include_entry_tags_and_contacts: bool | None | Unset - if isinstance(self.include_entry_tags_and_contacts, Unset): - include_entry_tags_and_contacts = UNSET - else: - include_entry_tags_and_contacts = self.include_entry_tags_and_contacts - collection_task_retrieve_offset: int | None | Unset if isinstance(self.collection_task_retrieve_offset, Unset): collection_task_retrieve_offset = UNSET else: collection_task_retrieve_offset = self.collection_task_retrieve_offset + include_entry_tags_and_contacts: bool | None | Unset + if isinstance(self.include_entry_tags_and_contacts, Unset): + include_entry_tags_and_contacts = UNSET + else: + include_entry_tags_and_contacts = self.include_entry_tags_and_contacts + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -68,10 +68,10 @@ def to_dict(self) -> dict[str, Any]: field_dict["allow_archived"] = allow_archived if allow_archived_entries is not UNSET: field_dict["allow_archived_entries"] = allow_archived_entries - if include_entry_tags_and_contacts is not UNSET: - field_dict["include_entry_tags_and_contacts"] = include_entry_tags_and_contacts if collection_task_retrieve_offset is not UNSET: field_dict["collection_task_retrieve_offset"] = collection_task_retrieve_offset + if include_entry_tags_and_contacts is not UNSET: + field_dict["include_entry_tags_and_contacts"] = include_entry_tags_and_contacts return field_dict @@ -98,34 +98,34 @@ def _parse_allow_archived_entries(data: object) -> bool | None | Unset: allow_archived_entries = _parse_allow_archived_entries(d.pop("allow_archived_entries", UNSET)) - def _parse_include_entry_tags_and_contacts(data: object) -> bool | None | Unset: + def _parse_collection_task_retrieve_offset(data: object) -> int | None | Unset: if data is None: return data if isinstance(data, Unset): return data - return cast(bool | None | Unset, data) + return cast(int | None | Unset, data) - include_entry_tags_and_contacts = _parse_include_entry_tags_and_contacts( - d.pop("include_entry_tags_and_contacts", UNSET) + collection_task_retrieve_offset = _parse_collection_task_retrieve_offset( + d.pop("collection_task_retrieve_offset", UNSET) ) - def _parse_collection_task_retrieve_offset(data: object) -> int | None | Unset: + def _parse_include_entry_tags_and_contacts(data: object) -> bool | None | Unset: if data is None: return data if isinstance(data, Unset): return data - return cast(int | None | Unset, data) + return cast(bool | None | Unset, data) - collection_task_retrieve_offset = _parse_collection_task_retrieve_offset( - d.pop("collection_task_retrieve_offset", UNSET) + include_entry_tags_and_contacts = _parse_include_entry_tags_and_contacts( + d.pop("include_entry_tags_and_contacts", UNSET) ) metric_load_args = cls( ref_id=ref_id, allow_archived=allow_archived, allow_archived_entries=allow_archived_entries, - include_entry_tags_and_contacts=include_entry_tags_and_contacts, collection_task_retrieve_offset=collection_task_retrieve_offset, + include_entry_tags_and_contacts=include_entry_tags_and_contacts, ) metric_load_args.additional_properties = d diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_public_args.py index 6ca4c82bc..b3eb29d33 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_public_args.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_public_args.py @@ -57,7 +57,9 @@ def _parse_include_entry_tags_and_contacts(data: object) -> bool | None | Unset: return data return cast(bool | None | Unset, data) - include_entry_tags_and_contacts = _parse_include_entry_tags_and_contacts(d.pop("include_entry_tags_and_contacts", UNSET)) + include_entry_tags_and_contacts = _parse_include_entry_tags_and_contacts( + d.pop("include_entry_tags_and_contacts", UNSET) + ) metric_load_public_args = cls( external_id=external_id, diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_result.py index 860c72fad..b88b32962 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_result.py @@ -13,7 +13,9 @@ from ..models.metric import Metric from ..models.metric_entry import MetricEntry from ..models.metric_load_metric_entry_tags import MetricLoadMetricEntryTags + from ..models.metric_load_result_metric_entry_contacts_type_0 import MetricLoadResultMetricEntryContactsType0 from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag @@ -33,6 +35,8 @@ class MetricLoadResult: collection_tasks_total_cnt (int): collection_tasks_page_size (int): note (None | Note | Unset): + metric_entry_contacts (MetricLoadResultMetricEntryContactsType0 | None | Unset): + publish_entity (None | PublishEntity | Unset): """ metric: Metric @@ -43,10 +47,14 @@ class MetricLoadResult: collection_tasks_total_cnt: int collection_tasks_page_size: int note: None | Note | Unset = UNSET + metric_entry_contacts: MetricLoadResultMetricEntryContactsType0 | None | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.metric_load_result_metric_entry_contacts_type_0 import MetricLoadResultMetricEntryContactsType0 from ..models.note import Note + from ..models.publish_entity import PublishEntity metric = self.metric.to_dict() @@ -82,6 +90,22 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + metric_entry_contacts: dict[str, Any] | None | Unset + if isinstance(self.metric_entry_contacts, Unset): + metric_entry_contacts = UNSET + elif isinstance(self.metric_entry_contacts, MetricLoadResultMetricEntryContactsType0): + metric_entry_contacts = self.metric_entry_contacts.to_dict() + else: + metric_entry_contacts = self.metric_entry_contacts + + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -97,6 +121,10 @@ def to_dict(self) -> dict[str, Any]: ) if note is not UNSET: field_dict["note"] = note + if metric_entry_contacts is not UNSET: + field_dict["metric_entry_contacts"] = metric_entry_contacts + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -106,7 +134,9 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.metric import Metric from ..models.metric_entry import MetricEntry from ..models.metric_load_metric_entry_tags import MetricLoadMetricEntryTags + from ..models.metric_load_result_metric_entry_contacts_type_0 import MetricLoadResultMetricEntryContactsType0 from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.tag import Tag d = dict(src_dict) @@ -161,6 +191,40 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_metric_entry_contacts(data: object) -> MetricLoadResultMetricEntryContactsType0 | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + metric_entry_contacts_type_0 = MetricLoadResultMetricEntryContactsType0.from_dict(data) + + return metric_entry_contacts_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(MetricLoadResultMetricEntryContactsType0 | None | Unset, data) + + metric_entry_contacts = _parse_metric_entry_contacts(d.pop("metric_entry_contacts", UNSET)) + + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + metric_load_result = cls( metric=metric, tags=tags, @@ -170,6 +234,8 @@ def _parse_note(data: object) -> None | Note | Unset: collection_tasks_total_cnt=collection_tasks_total_cnt, collection_tasks_page_size=collection_tasks_page_size, note=note, + metric_entry_contacts=metric_entry_contacts, + publish_entity=publish_entity, ) metric_load_result.additional_properties = d diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_result_metric_entry_contacts_type_0.py b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_result_metric_entry_contacts_type_0.py new file mode 100644 index 000000000..32cd24aad --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/metric_load_result_metric_entry_contacts_type_0.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.contact import Contact + + +T = TypeVar("T", bound="MetricLoadResultMetricEntryContactsType0") + + +@_attrs_define +class MetricLoadResultMetricEntryContactsType0: + """ """ + + additional_properties: dict[str, list[Contact]] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = [] + for additional_property_item_data in prop: + additional_property_item = additional_property_item_data.to_dict() + field_dict[prop_name].append(additional_property_item) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.contact import Contact + + d = dict(src_dict) + metric_load_result_metric_entry_contacts_type_0 = cls() + + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = [] + _additional_property = prop_dict + for additional_property_item_data in _additional_property: + additional_property_item = Contact.from_dict(additional_property_item_data) + + additional_property.append(additional_property_item) + + additional_properties[prop_name] = additional_property + + metric_load_result_metric_entry_contacts_type_0.additional_properties = additional_properties + return metric_load_result_metric_entry_contacts_type_0 + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> list[Contact]: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: list[Contact]) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_public_from_schedule_stream_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_public_from_schedule_stream_args.py new file mode 100644 index 000000000..93a59e284 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_full_days_load_public_from_schedule_stream_args.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs") + + +@_attrs_define +class ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs: + """ScheduleEventFullDaysLoadPublicFromScheduleStream args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + ref_id (str): A generic entity id. + """ + + external_id: str + ref_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + ref_id = self.ref_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + "ref_id": ref_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + ref_id = d.pop("ref_id") + + schedule_event_full_days_load_public_from_schedule_stream_args = cls( + external_id=external_id, + ref_id=ref_id, + ) + + schedule_event_full_days_load_public_from_schedule_stream_args.additional_properties = d + return schedule_event_full_days_load_public_from_schedule_stream_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_public_from_schedule_stream_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_public_from_schedule_stream_args.py new file mode 100644 index 000000000..cc6343304 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_event_in_day_load_public_from_schedule_stream_args.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ScheduleEventInDayLoadPublicFromScheduleStreamArgs") + + +@_attrs_define +class ScheduleEventInDayLoadPublicFromScheduleStreamArgs: + """ScheduleEventInDayLoadPublicFromScheduleStream args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + ref_id (str): A generic entity id. + """ + + external_id: str + ref_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + ref_id = self.ref_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + "ref_id": ref_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + ref_id = d.pop("ref_id") + + schedule_event_in_day_load_public_from_schedule_stream_args = cls( + external_id=external_id, + ref_id=ref_id, + ) + + schedule_event_in_day_load_public_from_schedule_stream_args.additional_properties = d + return schedule_event_in_day_load_public_from_schedule_stream_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/schedule_stream_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_stream_load_public_args.py new file mode 100644 index 000000000..4c1ab5c56 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_stream_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="ScheduleStreamLoadPublicArgs") + + +@_attrs_define +class ScheduleStreamLoadPublicArgs: + """ScheduleStreamLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + schedule_stream_load_public_args = cls( + external_id=external_id, + ) + + schedule_stream_load_public_args.additional_properties = d + return schedule_stream_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/schedule_stream_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_stream_load_result.py index e841ad191..c7e7e00c6 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/schedule_stream_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/schedule_stream_load_result.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.schedule_stream import ScheduleStream from ..models.tag import Tag @@ -19,21 +20,24 @@ @_attrs_define class ScheduleStreamLoadResult: - """Result. + """ScheduleStreamLoadResult. Attributes: schedule_stream (ScheduleStream): A schedule group or stream of events. tags (list[Tag]): note (None | Note | Unset): + publish_entity (None | PublishEntity | Unset): """ schedule_stream: ScheduleStream tags: list[Tag] note: None | Note | Unset = UNSET + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: from ..models.note import Note + from ..models.publish_entity import PublishEntity schedule_stream = self.schedule_stream.to_dict() @@ -50,6 +54,14 @@ def to_dict(self) -> dict[str, Any]: else: note = self.note + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -60,12 +72,15 @@ def to_dict(self) -> dict[str, Any]: ) if note is not UNSET: field_dict["note"] = note + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.schedule_stream import ScheduleStream from ..models.tag import Tag @@ -96,10 +111,28 @@ def _parse_note(data: object) -> None | Note | Unset: note = _parse_note(d.pop("note", UNSET)) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + schedule_stream_load_result = cls( schedule_stream=schedule_stream, tags=tags, note=note, + publish_entity=publish_entity, ) schedule_stream_load_result.additional_properties = d diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index 38d8edfe1..f17b2db1e 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -91,6 +91,7 @@ export type { CalendarEventsStats } from './models/CalendarEventsStats'; export type { CalendarEventsStatsPerSubperiod } from './models/CalendarEventsStatsPerSubperiod'; export type { CalendarLoadForDateAndPeriodArgs } from './models/CalendarLoadForDateAndPeriodArgs'; export type { CalendarLoadForDateAndPeriodResult } from './models/CalendarLoadForDateAndPeriodResult'; +export type { CalendarLoadPublicForScheduleStreamArgs } from './models/CalendarLoadPublicForScheduleStreamArgs'; export type { ChangePasswordArgs } from './models/ChangePasswordArgs'; export type { Chapter } from './models/Chapter'; export type { ChapterArchiveArgs } from './models/ChapterArchiveArgs'; @@ -413,8 +414,8 @@ export type { MetricFindArgs } from './models/MetricFindArgs'; export type { MetricFindResponseEntry } from './models/MetricFindResponseEntry'; export type { MetricFindResult } from './models/MetricFindResult'; export type { MetricLoadArgs } from './models/MetricLoadArgs'; -export type { MetricLoadPublicArgs } from './models/MetricLoadPublicArgs'; export type { MetricLoadMetricEntryTags } from './models/MetricLoadMetricEntryTags'; +export type { MetricLoadPublicArgs } from './models/MetricLoadPublicArgs'; export type { MetricLoadResult } from './models/MetricLoadResult'; export type { MetricLoadSettingsArgs } from './models/MetricLoadSettingsArgs'; export type { MetricLoadSettingsResult } from './models/MetricLoadSettingsResult'; @@ -547,6 +548,7 @@ export type { ScheduleEventFullDaysCreateArgs } from './models/ScheduleEventFull export type { ScheduleEventFullDaysCreateResult } from './models/ScheduleEventFullDaysCreateResult'; export type { ScheduleEventFullDaysLoadArgs } from './models/ScheduleEventFullDaysLoadArgs'; export type { ScheduleEventFullDaysLoadPublicArgs } from './models/ScheduleEventFullDaysLoadPublicArgs'; +export type { ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs } from './models/ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs'; export type { ScheduleEventFullDaysLoadResult } from './models/ScheduleEventFullDaysLoadResult'; export type { ScheduleEventFullDaysName } from './models/ScheduleEventFullDaysName'; export type { ScheduleEventFullDaysRemoveArgs } from './models/ScheduleEventFullDaysRemoveArgs'; @@ -558,6 +560,7 @@ export type { ScheduleEventInDayCreateArgs } from './models/ScheduleEventInDayCr export type { ScheduleEventInDayCreateResult } from './models/ScheduleEventInDayCreateResult'; export type { ScheduleEventInDayLoadArgs } from './models/ScheduleEventInDayLoadArgs'; export type { ScheduleEventInDayLoadPublicArgs } from './models/ScheduleEventInDayLoadPublicArgs'; +export type { ScheduleEventInDayLoadPublicFromScheduleStreamArgs } from './models/ScheduleEventInDayLoadPublicFromScheduleStreamArgs'; export type { ScheduleEventInDayLoadResult } from './models/ScheduleEventInDayLoadResult'; export type { ScheduleEventInDayName } from './models/ScheduleEventInDayName'; export type { ScheduleEventInDayRemoveArgs } from './models/ScheduleEventInDayRemoveArgs'; @@ -598,6 +601,7 @@ export type { ScheduleStreamFindArgs } from './models/ScheduleStreamFindArgs'; export type { ScheduleStreamFindResult } from './models/ScheduleStreamFindResult'; export type { ScheduleStreamFindResultEntry } from './models/ScheduleStreamFindResultEntry'; export type { ScheduleStreamLoadArgs } from './models/ScheduleStreamLoadArgs'; +export type { ScheduleStreamLoadPublicArgs } from './models/ScheduleStreamLoadPublicArgs'; export type { ScheduleStreamLoadResult } from './models/ScheduleStreamLoadResult'; export type { ScheduleStreamName } from './models/ScheduleStreamName'; export type { ScheduleStreamRemoveArgs } from './models/ScheduleStreamRemoveArgs'; diff --git a/gen/ts/webapi-client/gen/models/CalendarLoadPublicForScheduleStreamArgs.ts b/gen/ts/webapi-client/gen/models/CalendarLoadPublicForScheduleStreamArgs.ts new file mode 100644 index 000000000..b1d1e81ab --- /dev/null +++ b/gen/ts/webapi-client/gen/models/CalendarLoadPublicForScheduleStreamArgs.ts @@ -0,0 +1,17 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ADate } from './ADate'; +import type { PublishExternalId } from './PublishExternalId'; +import type { RecurringTaskPeriod } from './RecurringTaskPeriod'; +/** + * CalendarLoadPublicForScheduleStream args. + */ +export type CalendarLoadPublicForScheduleStreamArgs = { + external_id: PublishExternalId; + right_now: ADate; + period: RecurringTaskPeriod; + stats_subperiod?: (RecurringTaskPeriod | null); +}; + diff --git a/gen/ts/webapi-client/gen/models/MetricEntryLoadPublicFromMetricArgs.ts b/gen/ts/webapi-client/gen/models/MetricEntryLoadPublicFromMetricArgs.ts index b61c079ef..0df8d6b2e 100644 --- a/gen/ts/webapi-client/gen/models/MetricEntryLoadPublicFromMetricArgs.ts +++ b/gen/ts/webapi-client/gen/models/MetricEntryLoadPublicFromMetricArgs.ts @@ -11,3 +11,4 @@ export type MetricEntryLoadPublicFromMetricArgs = { external_id: PublishExternalId; ref_id: EntityId; }; + diff --git a/gen/ts/webapi-client/gen/models/MetricLoadArgs.ts b/gen/ts/webapi-client/gen/models/MetricLoadArgs.ts index b15e2afa1..206768b68 100644 --- a/gen/ts/webapi-client/gen/models/MetricLoadArgs.ts +++ b/gen/ts/webapi-client/gen/models/MetricLoadArgs.ts @@ -10,7 +10,7 @@ export type MetricLoadArgs = { ref_id: EntityId; allow_archived?: (boolean | null); allow_archived_entries?: (boolean | null); - include_entry_tags_and_contacts?: (boolean | null); collection_task_retrieve_offset?: (number | null); + include_entry_tags_and_contacts?: (boolean | null); }; diff --git a/gen/ts/webapi-client/gen/models/MetricLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/MetricLoadPublicArgs.ts index af97f5d7b..a23b26808 100644 --- a/gen/ts/webapi-client/gen/models/MetricLoadPublicArgs.ts +++ b/gen/ts/webapi-client/gen/models/MetricLoadPublicArgs.ts @@ -10,3 +10,4 @@ export type MetricLoadPublicArgs = { external_id: PublishExternalId; include_entry_tags_and_contacts?: (boolean | null); }; + diff --git a/gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs.ts b/gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs.ts new file mode 100644 index 000000000..6590d5426 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { PublishExternalId } from './PublishExternalId'; +/** + * ScheduleEventFullDaysLoadPublicFromScheduleStream args. + */ +export type ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs = { + external_id: PublishExternalId; + ref_id: EntityId; +}; + diff --git a/gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadPublicFromScheduleStreamArgs.ts b/gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadPublicFromScheduleStreamArgs.ts new file mode 100644 index 000000000..8a2863ed4 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/ScheduleEventInDayLoadPublicFromScheduleStreamArgs.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { PublishExternalId } from './PublishExternalId'; +/** + * ScheduleEventInDayLoadPublicFromScheduleStream args. + */ +export type ScheduleEventInDayLoadPublicFromScheduleStreamArgs = { + external_id: PublishExternalId; + ref_id: EntityId; +}; + diff --git a/gen/ts/webapi-client/gen/models/ScheduleStreamLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/ScheduleStreamLoadPublicArgs.ts new file mode 100644 index 000000000..0ef765187 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/ScheduleStreamLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * ScheduleStreamLoadPublic args. + */ +export type ScheduleStreamLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/ScheduleStreamLoadResult.ts b/gen/ts/webapi-client/gen/models/ScheduleStreamLoadResult.ts index 8ba6dd21b..26e8c7fe7 100644 --- a/gen/ts/webapi-client/gen/models/ScheduleStreamLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/ScheduleStreamLoadResult.ts @@ -3,14 +3,16 @@ /* tslint:disable */ /* eslint-disable */ import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { ScheduleStream } from './ScheduleStream'; import type { Tag } from './Tag'; /** - * Result. + * ScheduleStreamLoadResult. */ export type ScheduleStreamLoadResult = { schedule_stream: ScheduleStream; note?: (Note | null); tags: Array<Tag>; + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/services/CalendarService.ts b/gen/ts/webapi-client/gen/services/CalendarService.ts index ec47d73f4..f6e5291de 100644 --- a/gen/ts/webapi-client/gen/services/CalendarService.ts +++ b/gen/ts/webapi-client/gen/services/CalendarService.ts @@ -4,6 +4,7 @@ /* eslint-disable */ import type { CalendarLoadForDateAndPeriodArgs } from '../models/CalendarLoadForDateAndPeriodArgs'; import type { CalendarLoadForDateAndPeriodResult } from '../models/CalendarLoadForDateAndPeriodResult'; +import type { CalendarLoadPublicForScheduleStreamArgs } from '../models/CalendarLoadPublicForScheduleStreamArgs'; import type { CancelablePromise } from '../core/CancelablePromise'; import type { BaseHttpRequest } from '../core/BaseHttpRequest'; export class CalendarService { @@ -36,4 +37,32 @@ export class CalendarService { }, }); } + /** + * Load calendar entries and stats for a published schedule stream. + * @param requestBody The input data + * @returns CalendarLoadForDateAndPeriodResult Successful response + * @throws ApiError + */ + public calendarLoadPublicForScheduleStream( + requestBody?: CalendarLoadPublicForScheduleStreamArgs, + ): CancelablePromise<CalendarLoadForDateAndPeriodResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/calendar-load-public-for-schedule-stream', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } } diff --git a/gen/ts/webapi-client/gen/services/MetricsService.ts b/gen/ts/webapi-client/gen/services/MetricsService.ts index 2d7d56d60..17cd84c62 100644 --- a/gen/ts/webapi-client/gen/services/MetricsService.ts +++ b/gen/ts/webapi-client/gen/services/MetricsService.ts @@ -309,17 +309,17 @@ export class MetricsService { }); } /** - * Load a published metric by publish external id. + * Use case for loading a metric. * @param requestBody The input data * @returns MetricLoadResult Successful response * @throws ApiError */ - public metricLoadPublic( - requestBody?: MetricLoadPublicArgs, + public metricLoad( + requestBody?: MetricLoadArgs, ): CancelablePromise<MetricLoadResult> { return this.httpRequest.request({ method: 'POST', - url: '/metric-load-public', + url: '/metric-load', body: requestBody, mediaType: 'application/json', errors: { @@ -337,17 +337,17 @@ export class MetricsService { }); } /** - * Use case for loading a metric. + * Load a published metric by publish external id. * @param requestBody The input data * @returns MetricLoadResult Successful response * @throws ApiError */ - public metricLoad( - requestBody?: MetricLoadArgs, + public metricLoadPublic( + requestBody?: MetricLoadPublicArgs, ): CancelablePromise<MetricLoadResult> { return this.httpRequest.request({ method: 'POST', - url: '/metric-load', + url: '/metric-load-public', body: requestBody, mediaType: 'application/json', errors: { diff --git a/gen/ts/webapi-client/gen/services/ScheduleService.ts b/gen/ts/webapi-client/gen/services/ScheduleService.ts index 4c1a991e5..898eaa8b6 100644 --- a/gen/ts/webapi-client/gen/services/ScheduleService.ts +++ b/gen/ts/webapi-client/gen/services/ScheduleService.ts @@ -8,6 +8,7 @@ import type { ScheduleEventFullDaysCreateArgs } from '../models/ScheduleEventFul import type { ScheduleEventFullDaysCreateResult } from '../models/ScheduleEventFullDaysCreateResult'; import type { ScheduleEventFullDaysLoadArgs } from '../models/ScheduleEventFullDaysLoadArgs'; import type { ScheduleEventFullDaysLoadPublicArgs } from '../models/ScheduleEventFullDaysLoadPublicArgs'; +import type { ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs } from '../models/ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs'; import type { ScheduleEventFullDaysLoadResult } from '../models/ScheduleEventFullDaysLoadResult'; import type { ScheduleEventFullDaysRemoveArgs } from '../models/ScheduleEventFullDaysRemoveArgs'; import type { ScheduleEventFullDaysUpdateArgs } from '../models/ScheduleEventFullDaysUpdateArgs'; @@ -17,6 +18,7 @@ import type { ScheduleEventInDayCreateArgs } from '../models/ScheduleEventInDayC import type { ScheduleEventInDayCreateResult } from '../models/ScheduleEventInDayCreateResult'; import type { ScheduleEventInDayLoadArgs } from '../models/ScheduleEventInDayLoadArgs'; import type { ScheduleEventInDayLoadPublicArgs } from '../models/ScheduleEventInDayLoadPublicArgs'; +import type { ScheduleEventInDayLoadPublicFromScheduleStreamArgs } from '../models/ScheduleEventInDayLoadPublicFromScheduleStreamArgs'; import type { ScheduleEventInDayLoadResult } from '../models/ScheduleEventInDayLoadResult'; import type { ScheduleEventInDayRemoveArgs } from '../models/ScheduleEventInDayRemoveArgs'; import type { ScheduleEventInDayUpdateArgs } from '../models/ScheduleEventInDayUpdateArgs'; @@ -42,6 +44,7 @@ import type { ScheduleStreamCreateForUserResult } from '../models/ScheduleStream import type { ScheduleStreamFindArgs } from '../models/ScheduleStreamFindArgs'; import type { ScheduleStreamFindResult } from '../models/ScheduleStreamFindResult'; import type { ScheduleStreamLoadArgs } from '../models/ScheduleStreamLoadArgs'; +import type { ScheduleStreamLoadPublicArgs } from '../models/ScheduleStreamLoadPublicArgs'; import type { ScheduleStreamLoadResult } from '../models/ScheduleStreamLoadResult'; import type { ScheduleStreamRemoveArgs } from '../models/ScheduleStreamRemoveArgs'; import type { ScheduleStreamUpdateArgs } from '../models/ScheduleStreamUpdateArgs'; @@ -189,6 +192,34 @@ export class ScheduleService { }, }); } + /** + * Load a schedule full days event through a published schedule stream. + * @param requestBody The input data + * @returns ScheduleEventFullDaysLoadResult Successful response + * @throws ApiError + */ + public scheduleEventFullDaysLoadPublicFromScheduleStream( + requestBody?: ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs, + ): CancelablePromise<ScheduleEventFullDaysLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/schedule-event-full-days-load-public-from-schedule-stream', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * Use case for removing a full day event. * @param requestBody The input data @@ -385,6 +416,34 @@ export class ScheduleService { }, }); } + /** + * Load a schedule event in day through a published schedule stream. + * @param requestBody The input data + * @returns ScheduleEventInDayLoadResult Successful response + * @throws ApiError + */ + public scheduleEventInDayLoadPublicFromScheduleStream( + requestBody?: ScheduleEventInDayLoadPublicFromScheduleStreamArgs, + ): CancelablePromise<ScheduleEventInDayLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/schedule-event-in-day-load-public-from-schedule-stream', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * Use case for removing a schedule in day event. * @param requestBody The input data @@ -833,6 +892,34 @@ export class ScheduleService { }, }); } + /** + * Load a published schedule stream by publish external id. + * @param requestBody The input data + * @returns ScheduleStreamLoadResult Successful response + * @throws ApiError + */ + public scheduleStreamLoadPublic( + requestBody?: ScheduleStreamLoadPublicArgs, + ): CancelablePromise<ScheduleStreamLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/schedule-stream-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * Use case for removing a schedule stream. * @param requestBody The input data diff --git a/itests/webui/entities/schedules.test.py b/itests/webui/entities/schedules.test.py index 01934afc6..76ece1f3b 100644 --- a/itests/webui/entities/schedules.test.py +++ b/itests/webui/entities/schedules.test.py @@ -237,6 +237,53 @@ def test_webui_schedule_event_in_day_publish_and_view_public( expect(page.locator('input[name="startTimeInDay"]')).to_have_value("09:00") +def test_webui_schedule_stream_publish_and_view_public( + page: Page, create_schedule_stream, create_schedule_event_in_day +) -> None: + schedule_stream = create_schedule_stream("Published Stream Calendar") + event = create_schedule_event_in_day( + schedule_stream.ref_id, + "Stream Calendar Event", + "2024-07-01", + "09:00", + 60, + ) + + page.goto(f"/app/workspace/calendar/schedule/stream/{schedule_stream.ref_id}") + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "ScheduleStream-publish") + page.locator("button[id='ScheduleStream-publish-create']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/calendar/schedule/stream/{schedule_stream.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + open_leaf_publish_panel(page, "ScheduleStream-publish") + page.locator("button[id='ScheduleStream-publish-toggle-status']").click() + page.wait_for_url( + re.compile(rf"/app/workspace/calendar/schedule/stream/{schedule_stream.ref_id}") + ) + page.wait_for_selector("#leaf-panel") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url(re.compile(r"/app/public/published/schedule-stream/")) + page.wait_for_selector("#leaf-panel") + + page.goto(f"{public_url.split('?')[0]}?date=2024-07-01&period=daily&view=calendar") + page.wait_for_selector("#leaf-panel") + expect( + page.locator(f"#schedule-event-in-day-block-{event.ref_id}") + ).to_contain_text(re.compile(r".*Stream Calendar.*")) + + page.locator(f"#schedule-event-in-day-block-{event.ref_id}").click() + page.wait_for_selector("#leaf-panel") + expect(page.locator('input[name="name"]')).to_have_value("Stream Calendar Event") + + def test_webui_schedule_event_full_days_publish_and_view_public( page: Page, create_schedule_stream, create_schedule_event_full_days ) -> None: diff --git a/src/core/jupiter/core/calendar/component/calendar-navigation.tsx b/src/core/jupiter/core/calendar/component/calendar-navigation.tsx new file mode 100644 index 000000000..bd4b0b619 --- /dev/null +++ b/src/core/jupiter/core/calendar/component/calendar-navigation.tsx @@ -0,0 +1,135 @@ +import { RecurringTaskPeriod } from "@jupiter/webapi-client"; +import { Box } from "@mui/material"; +import { createContext, PropsWithChildren, ReactNode, useContext } from "react"; +import { useSearchParams } from "@remix-run/react"; + +import { EntityLink } from "#/core/infra/component/entity-card"; + +export type CalendarEventLinkKind = + | "schedule-event-in-day" + | "schedule-event-full-days" + | "time-event-in-day-block" + | "time-event-full-days-block"; + +export interface CalendarNavigationValue { + eventPath: (kind: CalendarEventLinkKind, refId: string) => string | undefined; + statsPath: ( + calendarLocation: string, + periodStartDate: string, + period: RecurringTaskPeriod, + view: string, + ) => string; +} + +function workspaceCalendarNavigation(): CalendarNavigationValue { + const calendarBasePath = "/app/workspace/calendar"; + return { + eventPath: (kind, refId) => { + switch (kind) { + case "schedule-event-in-day": + return `${calendarBasePath}/schedule/event-in-day/${refId}`; + case "schedule-event-full-days": + return `${calendarBasePath}/schedule/event-full-days/${refId}`; + case "time-event-in-day-block": + return `${calendarBasePath}/time-event/in-day-block/${refId}`; + case "time-event-full-days-block": + return `${calendarBasePath}/time-event/full-days-block/${refId}`; + } + }, + statsPath: (calendarLocation, periodStartDate, period, view) => + `${calendarBasePath}${calendarLocation}?date=${periodStartDate}&period=${period}&view=${view}`, + }; +} + +export function publishedScheduleStreamCalendarNavigation( + externalId: string, +): CalendarNavigationValue { + const calendarBasePath = `/app/public/published/schedule-stream/${externalId}`; + return { + eventPath: (kind, refId) => { + switch (kind) { + case "schedule-event-in-day": + return `${calendarBasePath}/in-day-event/${refId}`; + case "schedule-event-full-days": + return `${calendarBasePath}/full-days-event/${refId}`; + default: + return undefined; + } + }, + statsPath: (calendarLocation, periodStartDate, period, view) => + `${calendarBasePath}${calendarLocation}?date=${periodStartDate}&period=${period}&view=${view}`, + }; +} + +const CalendarNavigationContext = createContext<CalendarNavigationValue>( + workspaceCalendarNavigation(), +); + +export function CalendarNavigationProvider( + props: PropsWithChildren<{ value: CalendarNavigationValue }>, +) { + return ( + <CalendarNavigationContext.Provider value={props.value}> + {props.children} + </CalendarNavigationContext.Provider> + ); +} + +export function useCalendarNavigation(): CalendarNavigationValue { + return useContext(CalendarNavigationContext); +} + +interface CalendarEventLinkProps { + kind: CalendarEventLinkKind; + refId: string; + children: ReactNode; + inline?: boolean; + block?: boolean; + light?: boolean; +} + +export function CalendarEventLink(props: CalendarEventLinkProps) { + const navigation = useCalendarNavigation(); + const [query] = useSearchParams(); + const basePath = navigation.eventPath(props.kind, props.refId); + + if (!basePath) { + return ( + <Box + component="span" + sx={{ + display: props.inline ? "inline" : "block", + width: props.block ? "100%" : undefined, + height: props.block ? "100%" : undefined, + }} + > + {props.children} + </Box> + ); + } + + const path = basePath.includes("?") + ? `${basePath}&${query}` + : `${basePath}?${query}`; + + return ( + <EntityLink + to={path} + inline={props.inline} + block={props.block} + light={props.light} + > + {props.children} + </EntityLink> + ); +} + +export function useCalendarStatsPath( + calendarLocation: string, + periodStartDate: string, + period: RecurringTaskPeriod, + view: string, +): string { + const navigation = useCalendarNavigation(); + return navigation.statsPath(calendarLocation, periodStartDate, period, view); +} diff --git a/src/core/jupiter/core/calendar/component/shared.tsx b/src/core/jupiter/core/calendar/component/shared.tsx index fe133c1aa..93d077ab1 100644 --- a/src/core/jupiter/core/calendar/component/shared.tsx +++ b/src/core/jupiter/core/calendar/component/shared.tsx @@ -72,6 +72,10 @@ import { import { useBigScreen } from "#/core/infra/component/use-big-screen"; import { EntityNameComponent } from "#/core/common/component/entity-name"; import { EntityLink } from "#/core/infra/component/entity-card"; +import { + CalendarEventLink, + useCalendarStatsPath, +} from "#/core/calendar/component/calendar-navigation"; import { TimeEventParamsNewPlaceholder } from "#/core/common/sub/time_events/component/params-new-placeholder"; export const MAX_VISIBLE_TIME_EVENT_FULL_DAYS = 3; @@ -306,7 +310,6 @@ interface ViewAsCalendarTimeEventFullDaysCellProps { export function ViewAsCalendarTimeEventFullDaysCell( props: ViewAsCalendarTimeEventFullDaysCellProps, ) { - const [query] = useSearchParams(); const containerRef = useRef<HTMLDivElement>(null); const [containerWidth, setContainerWidth] = useState(120); @@ -342,9 +345,10 @@ export function ViewAsCalendarTimeEventFullDaysCell( overflow: "hidden", }} > - <EntityLink + <CalendarEventLink key={`schedule-event-full-days-${fullDaysEntry.event.ref_id}`} - to={`/app/workspace/calendar/schedule/event-full-days/${fullDaysEntry.event.ref_id}?${query}`} + kind="schedule-event-full-days" + refId={fullDaysEntry.event.ref_id} inline block={props.isAdding} > @@ -354,7 +358,7 @@ export function ViewAsCalendarTimeEventFullDaysCell( fullDaysEntry.stream.color, )} /> - </EntityLink> + </CalendarEventLink> </Box> ); } @@ -389,9 +393,10 @@ export function ViewAsCalendarTimeEventFullDaysCell( overflow: "hidden", }} > - <EntityLink + <CalendarEventLink key={`birthday-event-${fullDaysEntry.contact.ref_id}`} - to={`/app/workspace/calendar/time-event/full-days-block/${fullDaysEntry.occasion_time_event.ref_id}?${query}`} + kind="time-event-full-days-block" + refId={fullDaysEntry.occasion_time_event.ref_id} inline block={props.isAdding} > @@ -401,7 +406,7 @@ export function ViewAsCalendarTimeEventFullDaysCell( BIRTHDAY_TIME_EVENT_COLOR, )} /> - </EntityLink> + </CalendarEventLink> </Box> ); } @@ -432,9 +437,10 @@ export function ViewAsCalendarTimeEventFullDaysCell( overflow: "hidden", }} > - <EntityLink + <CalendarEventLink key={`vacation-event-${fullDaysEntry.time_event.ref_id}`} - to={`/app/workspace/calendar/time-event/full-days-block/${fullDaysEntry.time_event.ref_id}?${query}`} + kind="time-event-full-days-block" + refId={fullDaysEntry.time_event.ref_id} inline block={props.isAdding} > @@ -444,7 +450,7 @@ export function ViewAsCalendarTimeEventFullDaysCell( VACATION_TIME_EVENT_COLOR, )} /> - </EntityLink> + </CalendarEventLink> </Box> ); } @@ -628,7 +634,6 @@ interface ViewAsCalendarTimeEventInDayCellProps { export function ViewAsCalendarTimeEventInDayCell( props: ViewAsCalendarTimeEventInDayCellProps, ) { - const [query] = useSearchParams(); const theme = useTheme(); const containerRef = useRef<HTMLDivElement>(null); @@ -691,9 +696,10 @@ export function ViewAsCalendarTimeEventInDayCell( zIndex: props.offset, }} > - <EntityLink + <CalendarEventLink key={`schedule-event-in-day-${scheduleEntry.event.ref_id}`} - to={`/app/workspace/calendar/schedule/event-in-day/${scheduleEntry.event.ref_id}?${query}`} + kind="schedule-event-in-day" + refId={scheduleEntry.event.ref_id} inline block={props.isAdding} > @@ -714,7 +720,7 @@ export function ViewAsCalendarTimeEventInDayCell( )} /> </Box> - </EntityLink> + </CalendarEventLink> </Box> ); } @@ -782,9 +788,10 @@ export function ViewAsCalendarTimeEventInDayCell( zIndex: props.offset, }} > - <EntityLink + <CalendarEventLink key={`big-plan-event-in-day-block-${props.entry.time_event_in_tz.ref_id}`} - to={`/app/workspace/calendar/time-event/in-day-block/${props.entry.time_event_in_tz.ref_id}?${query}`} + kind="time-event-in-day-block" + refId={props.entry.time_event_in_tz.ref_id} inline block={props.isAdding} > @@ -805,7 +812,7 @@ export function ViewAsCalendarTimeEventInDayCell( )} /> </Box> - </EntityLink> + </CalendarEventLink> </Box> ); } @@ -876,9 +883,10 @@ export function ViewAsCalendarTimeEventInDayCell( zIndex: props.offset, }} > - <EntityLink + <CalendarEventLink key={`todo-task-event-in-day-block-${props.entry.time_event_in_tz.ref_id}`} - to={`/app/workspace/calendar/time-event/in-day-block/${props.entry.time_event_in_tz.ref_id}?${query}`} + kind="time-event-in-day-block" + refId={props.entry.time_event_in_tz.ref_id} inline block={props.isAdding} > @@ -899,7 +907,7 @@ export function ViewAsCalendarTimeEventInDayCell( )} /> </Box> - </EntityLink> + </CalendarEventLink> </Box> ); } @@ -960,9 +968,10 @@ export function ViewAsCalendarTimeEventInDayCell( zIndex: props.offset, }} > - <EntityLink + <CalendarEventLink key={`habit-event-in-day-block-${props.entry.time_event_in_tz.ref_id}`} - to={`/app/workspace/calendar/time-event/in-day-block/${props.entry.time_event_in_tz.ref_id}?${query}`} + kind="time-event-in-day-block" + refId={props.entry.time_event_in_tz.ref_id} inline block={props.isAdding} > @@ -983,7 +992,7 @@ export function ViewAsCalendarTimeEventInDayCell( )} /> </Box> - </EntityLink> + </CalendarEventLink> </Box> ); } @@ -1044,9 +1053,10 @@ export function ViewAsCalendarTimeEventInDayCell( zIndex: props.offset, }} > - <EntityLink + <CalendarEventLink key={`chore-event-in-day-block-${props.entry.time_event_in_tz.ref_id}`} - to={`/app/workspace/calendar/time-event/in-day-block/${props.entry.time_event_in_tz.ref_id}?${query}`} + kind="time-event-in-day-block" + refId={props.entry.time_event_in_tz.ref_id} inline block={props.isAdding} > @@ -1067,7 +1077,7 @@ export function ViewAsCalendarTimeEventInDayCell( )} /> </Box> - </EntityLink> + </CalendarEventLink> </Box> ); } @@ -1130,9 +1140,10 @@ export function ViewAsCalendarTimeEventInDayCell( zIndex: props.offset, }} > - <EntityLink + <CalendarEventLink key={`time-plan-activity-event-in-day-block-${props.entry.time_event_in_tz.ref_id}`} - to={`/app/workspace/calendar/time-event/in-day-block/${props.entry.time_event_in_tz.ref_id}?${query}`} + kind="time-event-in-day-block" + refId={props.entry.time_event_in_tz.ref_id} inline block={props.isAdding} > @@ -1153,7 +1164,7 @@ export function ViewAsCalendarTimeEventInDayCell( )} /> </Box> - </EntityLink> + </CalendarEventLink> </Box> ); } @@ -1189,6 +1200,13 @@ interface ViewAsCalendarGoToCellProps { } export function ViewAsCalendarGoToCell(props: ViewAsCalendarGoToCellProps) { + const statsPath = useCalendarStatsPath( + props.calendarLocation, + props.periodStart, + props.period, + View.CALENDAR, + ); + return ( <Box sx={{ @@ -1200,9 +1218,7 @@ export function ViewAsCalendarGoToCell(props: ViewAsCalendarGoToCellProps) { justifyContent: "center", }} > - <EntityLink - to={`/app/workspace/calendar${props.calendarLocation}?date=${props.periodStart}&period=${props.period}&view=calendar`} - > + <EntityLink to={statsPath}> <Typography variant="h6">{props.label}</Typography> </EntityLink> </Box> @@ -1251,7 +1267,6 @@ interface ViewAsScheduleTimeEventFullDaysRowsProps { export function ViewAsScheduleTimeEventFullDaysRows( props: ViewAsScheduleTimeEventFullDaysRowsProps, ) { - const [query] = useSearchParams(); const isBigScreen = useBigScreen(); const { theType } = parseEntityLinkStd(props.entry.time_event.owner); @@ -1271,10 +1286,11 @@ export function ViewAsScheduleTimeEventFullDaysRows( color={scheduleStreamColorHex(fullDaysEntry.stream.color)} height="0.25rem" > - <EntityLink + <CalendarEventLink light key={`schedule-event-full-days-${fullDaysEntry.event.ref_id}`} - to={`/app/workspace/calendar/schedule/event-full-days/${fullDaysEntry.event.ref_id}?${query}`} + kind="schedule-event-full-days" + refId={fullDaysEntry.event.ref_id} inline block={props.isAdding} > @@ -1287,7 +1303,7 @@ export function ViewAsScheduleTimeEventFullDaysRows( fullDaysEntry.stream.color, )} /> - </EntityLink> + </CalendarEventLink> </ViewAsScheduleEventCell> </Fragment> ); @@ -1308,10 +1324,11 @@ export function ViewAsScheduleTimeEventFullDaysRows( color={scheduleStreamColorHex(BIRTHDAY_TIME_EVENT_COLOR)} height="0.25rem" > - <EntityLink + <CalendarEventLink light key={`schedule-event-full-days-${fullDaysEntry.occasion_time_event.ref_id}`} - to={`/app/workspace/calendar/time-event/full-days-block/${fullDaysEntry.occasion_time_event.ref_id}?${query}`} + kind="time-event-full-days-block" + refId={fullDaysEntry.occasion_time_event.ref_id} inline block={props.isAdding} > @@ -1325,7 +1342,7 @@ export function ViewAsScheduleTimeEventFullDaysRows( BIRTHDAY_TIME_EVENT_COLOR, )} /> - </EntityLink> + </CalendarEventLink> </ViewAsScheduleEventCell> </Fragment> ); @@ -1346,10 +1363,11 @@ export function ViewAsScheduleTimeEventFullDaysRows( color={scheduleStreamColorHex(VACATION_TIME_EVENT_COLOR)} height="0.25rem" > - <EntityLink + <CalendarEventLink light key={`schedule-event-full-days-${fullDaysEntry.time_event.ref_id}`} - to={`/app/workspace/calendar/time-event/full-days-block/${fullDaysEntry.time_event.ref_id}?${query}`} + kind="time-event-full-days-block" + refId={fullDaysEntry.time_event.ref_id} inline block={props.isAdding} > @@ -1359,7 +1377,7 @@ export function ViewAsScheduleTimeEventFullDaysRows( VACATION_TIME_EVENT_COLOR, )} /> - </EntityLink> + </CalendarEventLink> </ViewAsScheduleEventCell> </Fragment> ); @@ -1379,7 +1397,6 @@ interface ViewAsScheduleTimeEventInDaysRowsProps { export function ViewAsScheduleTimeEventInDaysRows( props: ViewAsScheduleTimeEventInDaysRowsProps, ) { - const [query] = useSearchParams(); const isBigScreen = useBigScreen(); const startTime = calculateStartTimeForTimeEvent( @@ -1405,10 +1422,11 @@ export function ViewAsScheduleTimeEventInDaysRows( props.entry.time_event_in_tz.duration_mins, )} > - <EntityLink + <CalendarEventLink light key={`schedule-event-in-day-${scheduleEntry.event.ref_id}`} - to={`/app/workspace/calendar/schedule/event-in-day/${scheduleEntry.event.ref_id}?${query}`} + kind="schedule-event-in-day" + refId={scheduleEntry.event.ref_id} inline block={props.isAdding} > @@ -1421,7 +1439,7 @@ export function ViewAsScheduleTimeEventInDaysRows( scheduleEntry.stream.color, )} /> - </EntityLink> + </CalendarEventLink> </ViewAsScheduleEventCell> </Fragment> ); @@ -1451,10 +1469,11 @@ export function ViewAsScheduleTimeEventInDaysRows( props.entry.time_event_in_tz.duration_mins, )} > - <EntityLink + <CalendarEventLink light key={`time-event-in-day-block-${props.entry.time_event_in_tz.ref_id}`} - to={`/app/workspace/calendar/time-event/in-day-block/${props.entry.time_event_in_tz.ref_id}?${query}`} + kind="time-event-in-day-block" + refId={props.entry.time_event_in_tz.ref_id} inline block={props.isAdding} > @@ -1464,7 +1483,7 @@ export function ViewAsScheduleTimeEventInDaysRows( BIG_PLAN_TIME_EVENT_COLOR, )} /> - </EntityLink> + </CalendarEventLink> </ViewAsScheduleEventCell> </Fragment> ); @@ -1494,10 +1513,11 @@ export function ViewAsScheduleTimeEventInDaysRows( props.entry.time_event_in_tz.duration_mins, )} > - <EntityLink + <CalendarEventLink light key={`time-event-in-day-block-${props.entry.time_event_in_tz.ref_id}`} - to={`/app/workspace/calendar/time-event/in-day-block/${props.entry.time_event_in_tz.ref_id}?${query}`} + kind="time-event-in-day-block" + refId={props.entry.time_event_in_tz.ref_id} inline block={props.isAdding} > @@ -1510,7 +1530,7 @@ export function ViewAsScheduleTimeEventInDaysRows( TODO_TASK_TIME_EVENT_COLOR, )} /> - </EntityLink> + </CalendarEventLink> </ViewAsScheduleEventCell> </Fragment> ); @@ -1533,10 +1553,11 @@ export function ViewAsScheduleTimeEventInDaysRows( props.entry.time_event_in_tz.duration_mins, )} > - <EntityLink + <CalendarEventLink light key={`time-event-in-day-block-${props.entry.time_event_in_tz.ref_id}`} - to={`/app/workspace/calendar/time-event/in-day-block/${props.entry.time_event_in_tz.ref_id}?${query}`} + kind="time-event-in-day-block" + refId={props.entry.time_event_in_tz.ref_id} inline block={props.isAdding} > @@ -1546,7 +1567,7 @@ export function ViewAsScheduleTimeEventInDaysRows( HABIT_TIME_EVENT_COLOR, )} /> - </EntityLink> + </CalendarEventLink> </ViewAsScheduleEventCell> </Fragment> ); @@ -1569,10 +1590,11 @@ export function ViewAsScheduleTimeEventInDaysRows( props.entry.time_event_in_tz.duration_mins, )} > - <EntityLink + <CalendarEventLink light key={`time-event-in-day-block-${props.entry.time_event_in_tz.ref_id}`} - to={`/app/workspace/calendar/time-event/in-day-block/${props.entry.time_event_in_tz.ref_id}?${query}`} + kind="time-event-in-day-block" + refId={props.entry.time_event_in_tz.ref_id} inline block={props.isAdding} > @@ -1582,7 +1604,7 @@ export function ViewAsScheduleTimeEventInDaysRows( CHORE_TIME_EVENT_COLOR, )} /> - </EntityLink> + </CalendarEventLink> </ViewAsScheduleEventCell> </Fragment> ); @@ -1605,10 +1627,11 @@ export function ViewAsScheduleTimeEventInDaysRows( props.entry.time_event_in_tz.duration_mins, )} > - <EntityLink + <CalendarEventLink light key={`time-event-in-day-block-${props.entry.time_event_in_tz.ref_id}`} - to={`/app/workspace/calendar/time-event/in-day-block/${props.entry.time_event_in_tz.ref_id}?${query}`} + kind="time-event-in-day-block" + refId={props.entry.time_event_in_tz.ref_id} inline block={props.isAdding} > @@ -1618,7 +1641,7 @@ export function ViewAsScheduleTimeEventInDaysRows( TIME_PLAN_ACTIVITY_TIME_EVENT_COLOR, )} /> - </EntityLink> + </CalendarEventLink> </ViewAsScheduleEventCell> </Fragment> ); @@ -1696,10 +1719,15 @@ interface ViewAsStatsPerSubperiodProps { } export function ViewAsStatsPerSubperiod(props: ViewAsStatsPerSubperiodProps) { + const statsPath = useCalendarStatsPath( + props.calendarLocation, + props.stats.period_start_date, + props.stats.period, + props.view, + ); + return ( - <EntityLink - to={`/app/workspace/calendar${props.calendarLocation}?date=${props.stats.period_start_date}&period=${props.stats.period}&view=${props.view}`} - > + <EntityLink to={statsPath}> <Box sx={{ display: "flex", diff --git a/src/core/jupiter/core/calendar/service/__init__.py b/src/core/jupiter/core/calendar/service/__init__.py new file mode 100644 index 000000000..aecd4d8ef --- /dev/null +++ b/src/core/jupiter/core/calendar/service/__init__.py @@ -0,0 +1 @@ +"""Calendar services.""" diff --git a/src/core/jupiter/core/calendar/service/load_for_date_and_period.py b/src/core/jupiter/core/calendar/service/load_for_date_and_period.py new file mode 100644 index 000000000..b28196d63 --- /dev/null +++ b/src/core/jupiter/core/calendar/service/load_for_date_and_period.py @@ -0,0 +1,856 @@ +"""Shared service for loading calendar data for a date and period.""" + +from typing import cast + +from jupiter.core.archival_reason import JupiterArchivalReason +from jupiter.core.big_plans.collection import BigPlanCollection +from jupiter.core.big_plans.root import BigPlan +from jupiter.core.calendar.use_case.load_for_date_and_period import ( + BigPlanEntry, + CalendarEventsEntries, + CalendarEventsStats, + CalendarEventsStatsPerSubperiod, + CalendarLoadForDateAndPeriodResult, + ChoreEntry, + HabitEntry, + PersonOccasionEntry, + ScheduleFullDaysEventEntry, + ScheduleInDayEventEntry, + TimePlanActivityEntry, + TodoTaskEntry, + VacationEntry, +) +from jupiter.core.chores.collection import ChoreCollection +from jupiter.core.chores.root import Chore +from jupiter.core.common import schedules +from jupiter.core.common.recurring_task_period import RecurringTaskPeriod +from jupiter.core.common.sub.contacts.root import ContactDomain +from jupiter.core.common.sub.contacts.sub.contact.root import Contact +from jupiter.core.common.sub.contacts.sub.link.root import ContactLink +from jupiter.core.common.sub.inbox_tasks.collection import InboxTaskCollection +from jupiter.core.common.sub.inbox_tasks.root import ( + InboxTask, + InboxTaskRepository, +) +from jupiter.core.common.sub.tags.root import TagDomain +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag +from jupiter.core.common.sub.time_events.domain import TimeEventDomain +from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( + TimeEventFullDaysBlock, + TimeEventFullDaysBlockRepository, +) +from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( + TimeEventInDayBlock, + TimeEventInDayBlockRepository, +) +from jupiter.core.habits.collection import HabitCollection +from jupiter.core.habits.root import Habit +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.prm.root import PRM +from jupiter.core.prm.sub.person.root import Person +from jupiter.core.prm.sub.person.sub.occasion.root import Occasion +from jupiter.core.schedule.domain import ScheduleDomain +from jupiter.core.schedule.sub.event_full_days.root import ScheduleEventFullDays +from jupiter.core.schedule.sub.event_in_day.root import ScheduleEventInDay +from jupiter.core.schedule.sub.stream.root import ScheduleStream +from jupiter.core.time_plans.sub.activity.root import TimePlanActivity +from jupiter.core.todo.domain import TodoDomain +from jupiter.core.todo.root import TodoTask +from jupiter.core.vacations.collection import VacationCollection +from jupiter.core.vacations.root import Vacation +from jupiter.core.workspaces.root import Workspace +from jupiter.framework.base.adate import ADate +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.base.entity_name import NOT_USED_NAME +from jupiter.framework.errors import InputValidationError +from jupiter.framework.storage.repository import DomainUnitOfWork + + +def _time_events_in_day_for_owner_type_unique( + time_events_in_day: list[TimeEventInDayBlock], + owner_type: str, +) -> dict[EntityId, TimeEventInDayBlock]: + return { + te.owner.ref_id: te + for te in time_events_in_day + if te.owner.the_type == owner_type + } + + +def _time_events_in_day_grouped_by_owner_ref_id( + time_events_in_day: list[TimeEventInDayBlock], + owner_type: str, +) -> dict[EntityId, list[TimeEventInDayBlock]]: + result: dict[EntityId, list[TimeEventInDayBlock]] = {} + for te in time_events_in_day: + if te.owner.the_type != owner_type: + continue + result.setdefault(te.owner.ref_id, []).append(te) + return result + + +class CalendarLoadForDateAndPeriodService: + """Shared service for loading calendar data for a date and period.""" + + @staticmethod + def validate_stats_subperiod( + period: RecurringTaskPeriod, + stats_subperiod: RecurringTaskPeriod | None, + ) -> None: + """Validate stats subperiod args.""" + if stats_subperiod is not None: + if period is RecurringTaskPeriod.DAILY: + raise InputValidationError( + "Stats subperiod is not allowed for daily period." + ) + elif stats_subperiod not in period.all_smaller_periods: + raise InputValidationError( + f"Stats subperiod {stats_subperiod} is not smaller than period {period}." + ) + + @staticmethod + def compute_schedules( + right_now: ADate, + period: RecurringTaskPeriod, + ) -> tuple[schedules.Schedule, schedules.Schedule, schedules.Schedule]: + """Compute the current, previous, and next schedules for a period.""" + schedule = schedules.get_schedule( + period=period, + right_now=right_now.to_timestamp_at_start_of_day(), + name=NOT_USED_NAME, + ) + prev_schedule = schedules.get_schedule( + period=period, + right_now=schedule.first_day.subtract_days( + 1 + ).to_timestamp_at_start_of_day(), + name=NOT_USED_NAME, + ) + next_schedule = schedules.get_schedule( + period=period, + right_now=schedule.end_day.add_days(1).to_timestamp_at_start_of_day(), + name=NOT_USED_NAME, + ) + return schedule, prev_schedule, next_schedule + + async def load( + self, + uow: DomainUnitOfWork, + workspace: Workspace, + right_now: ADate, + period: RecurringTaskPeriod, + stats_subperiod: RecurringTaskPeriod | None, + time_event_domain: TimeEventDomain, + schedule_domain: ScheduleDomain, + schedule_streams_by_ref_id: dict[EntityId, ScheduleStream], + schedule_stream_ref_id: EntityId | None = None, + ) -> CalendarLoadForDateAndPeriodResult: + """Load calendar entries and stats for a workspace and period.""" + schedule, prev_schedule, next_schedule = self.compute_schedules( + right_now, period + ) + + entries: CalendarEventsEntries | None = None + if period is RecurringTaskPeriod.DAILY or period is RecurringTaskPeriod.WEEKLY: + entries = await self.build_entries( + uow, + workspace, + schedule, + time_event_domain, + schedule_domain, + schedule_streams_by_ref_id, + schedule_stream_ref_id=schedule_stream_ref_id, + ) + + stats: CalendarEventsStats | None = None + if schedule.period != RecurringTaskPeriod.DAILY and stats_subperiod is not None: + stats = await self.build_stats( + uow, + schedule, + stats_subperiod, + time_event_domain, + schedule_domain=schedule_domain, + schedule_stream_ref_id=schedule_stream_ref_id, + ) + + return CalendarLoadForDateAndPeriodResult( + right_now=right_now, + period=period, + stats_subperiod=stats_subperiod, + period_start_date=schedule.first_day, + period_end_date=schedule.end_day, + prev_period_start_date=prev_schedule.first_day, + next_period_start_date=next_schedule.first_day, + entries=entries, + stats=stats, + ) + + async def build_entries( + self, + uow: DomainUnitOfWork, + workspace: Workspace, + schedule: schedules.Schedule, + time_event_domain: TimeEventDomain, + schedule_domain: ScheduleDomain, + schedule_streams_by_ref_id: dict[EntityId, ScheduleStream], + schedule_stream_ref_id: EntityId | None = None, + ) -> CalendarEventsEntries: + time_events_full_days: list[TimeEventFullDaysBlock] = await uow.get( + TimeEventFullDaysBlockRepository + ).find_all_between( + parent_ref_id=time_event_domain.ref_id, + start_date=schedule.first_day, + end_date=schedule.end_day, + ) + + time_events_in_day: list[TimeEventInDayBlock] = await uow.get( + TimeEventInDayBlockRepository + ).find_all_between( + parent_ref_id=time_event_domain.ref_id, + # Events can be at most 48hrs long, and to catch those that start before the period + # but end inside it we have this little approach. + start_date=schedule.first_day.subtract_days(2), + end_date=schedule.end_day, + ) + + time_events_full_days_for_schedule_events_full_days: dict[ + EntityId, TimeEventFullDaysBlock + ] = { + te.owner.ref_id: te + for te in time_events_full_days + if te.owner.the_type == NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value + } + schedule_events_full_days = [] + if len(time_events_full_days_for_schedule_events_full_days) > 0: + schedule_events_full_days = await uow.get_for( + ScheduleEventFullDays + ).find_all_generic( + parent_ref_id=schedule_domain.ref_id, + allow_archived=False, + ref_id=list(time_events_full_days_for_schedule_events_full_days.keys()), + ) + + tags_domain = await uow.get_for(TagDomain).load_by_parent(workspace.ref_id) + + full_days_tags_by_schedule_event_ref_id: dict[EntityId, list[Tag]] = {} + if schedule_events_full_days: + full_days_tag_links = await uow.get(TagLinkRepository).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=False, + owner=[ + EntityLink.std( + NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, + se.ref_id, + ) + for se in schedule_events_full_days + ], + ) + all_fd_tag_ref_ids: list[EntityId] = [] + for tl in full_days_tag_links: + all_fd_tag_ref_ids.extend(tl.ref_ids) + if all_fd_tag_ref_ids: + all_full_days_tags = await uow.get_for(Tag).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=False, + ref_id=list(set(all_fd_tag_ref_ids)), + ) + all_full_days_tags_by_ref_id = {t.ref_id: t for t in all_full_days_tags} + else: + all_full_days_tags_by_ref_id = {} + for tag_link in full_days_tag_links: + full_days_tags_by_schedule_event_ref_id[ + cast(EntityId, tag_link.owner.ref_id) + ] = [ + all_full_days_tags_by_ref_id[rid] + for rid in tag_link.ref_ids + if rid in all_full_days_tags_by_ref_id + ] + + schedule_event_full_days_entries = [ + ScheduleFullDaysEventEntry( + event=se, + tags=full_days_tags_by_schedule_event_ref_id.get(se.ref_id, []), + time_event=time_events_full_days_for_schedule_events_full_days[ + se.ref_id + ], + stream=schedule_streams_by_ref_id[se.schedule_stream_ref_id], + ) + for se in schedule_events_full_days + ] + + time_events_in_day_for_schedule_events_in_day = ( + _time_events_in_day_for_owner_type_unique( + time_events_in_day, + NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, + ) + ) + schedule_events_in_day = [] + if len(time_events_in_day_for_schedule_events_in_day) > 0: + schedule_events_in_day = await uow.get_for( + ScheduleEventInDay + ).find_all_generic( + parent_ref_id=schedule_domain.ref_id, + allow_archived=False, + ref_id=list(time_events_in_day_for_schedule_events_in_day.keys()), + ) + + in_day_tags_by_schedule_event_ref_id: dict[EntityId, list[Tag]] = {} + if schedule_events_in_day: + in_day_tag_links = await uow.get(TagLinkRepository).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=False, + owner=[ + EntityLink.std( + NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, se.ref_id + ) + for se in schedule_events_in_day + ], + ) + all_in_day_tag_ref_ids: list[EntityId] = [] + for tl in in_day_tag_links: + all_in_day_tag_ref_ids.extend(tl.ref_ids) + if all_in_day_tag_ref_ids: + all_in_day_tags = await uow.get_for(Tag).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=False, + ref_id=list(set(all_in_day_tag_ref_ids)), + ) + all_in_day_tags_by_ref_id = {t.ref_id: t for t in all_in_day_tags} + else: + all_in_day_tags_by_ref_id = {} + for tag_link in in_day_tag_links: + in_day_tags_by_schedule_event_ref_id[ + cast(EntityId, tag_link.owner.ref_id) + ] = [ + all_in_day_tags_by_ref_id[rid] + for rid in tag_link.ref_ids + if rid in all_in_day_tags_by_ref_id + ] + + schedule_event_in_day_entries = [ + ScheduleInDayEventEntry( + event=se, + tags=in_day_tags_by_schedule_event_ref_id.get(se.ref_id, []), + time_event=time_events_in_day_for_schedule_events_in_day[se.ref_id], + stream=schedule_streams_by_ref_id[se.schedule_stream_ref_id], + ) + for se in schedule_events_in_day + ] + + if schedule_stream_ref_id is not None: + schedule_event_full_days_entries = [ + entry + for entry in schedule_event_full_days_entries + if entry.event.schedule_stream_ref_id == schedule_stream_ref_id + ] + schedule_event_in_day_entries = [ + entry + for entry in schedule_event_in_day_entries + if entry.event.schedule_stream_ref_id == schedule_stream_ref_id + ] + return CalendarEventsEntries( + schedule_event_full_days_entries=schedule_event_full_days_entries, + schedule_event_in_day_entries=schedule_event_in_day_entries, + big_plan_entries=[], + todo_task_entries=[], + habit_entries=[], + chore_entries=[], + time_plan_activity_entries=[], + person_occasion_entries=[], + vacation_entries=[], + ) + + time_events_in_day_for_big_plans = _time_events_in_day_grouped_by_owner_ref_id( + time_events_in_day, + NamedEntityTag.BIG_PLAN.value, + ) + big_plans: list[BigPlan] = [] + if len(time_events_in_day_for_big_plans) > 0: + big_plan_collection = await uow.get_for(BigPlanCollection).load_by_parent( + workspace.ref_id, + ) + big_plans = await uow.get_for(BigPlan).find_all_generic( + parent_ref_id=big_plan_collection.ref_id, + allow_archived=JupiterArchivalReason.GC, + ref_id=list(time_events_in_day_for_big_plans.keys()), + ) + big_plan_entries = [ + BigPlanEntry( + big_plan=big_plan, + time_events=time_events_in_day_for_big_plans[big_plan.ref_id], + ) + for big_plan in big_plans + ] + + time_events_in_day_for_todo_tasks = _time_events_in_day_grouped_by_owner_ref_id( + time_events_in_day, + NamedEntityTag.TODO_TASK.value, + ) + todo_tasks: list[TodoTask] = [] + todo_task_inbox_tasks: dict[EntityId, InboxTask] = {} + if len(time_events_in_day_for_todo_tasks) > 0: + todo_domain = await uow.get_for(TodoDomain).load_by_parent( + workspace.ref_id, + ) + todo_tasks = await uow.get_for(TodoTask).find_all_generic( + parent_ref_id=todo_domain.ref_id, + allow_archived=JupiterArchivalReason.GC, + ref_id=list(time_events_in_day_for_todo_tasks.keys()), + ) + inbox_task_collection = await uow.get_for( + InboxTaskCollection + ).load_by_parent(workspace.ref_id) + linked_inbox_tasks = await uow.get( + InboxTaskRepository + ).find_all_for_owner_created_desc( + parent_ref_id=inbox_task_collection.ref_id, + owner=[ + EntityLink.std(NamedEntityTag.TODO_TASK.value, tt.ref_id) + for tt in todo_tasks + ], + allow_archived=JupiterArchivalReason.GC, + ) + for it in linked_inbox_tasks: + if it.owner.ref_id not in todo_task_inbox_tasks: + todo_task_inbox_tasks[it.owner.ref_id] = it + todo_task_entries = [ + TodoTaskEntry( + todo_task=todo_task, + inbox_task=todo_task_inbox_tasks[todo_task.ref_id], + time_events=time_events_in_day_for_todo_tasks[todo_task.ref_id], + ) + for todo_task in todo_tasks + if todo_task.ref_id in todo_task_inbox_tasks + ] + + time_events_in_day_for_habits = _time_events_in_day_grouped_by_owner_ref_id( + time_events_in_day, + NamedEntityTag.HABIT.value, + ) + habits: list[Habit] = [] + if len(time_events_in_day_for_habits) > 0: + habit_collection = await uow.get_for(HabitCollection).load_by_parent( + workspace.ref_id, + ) + habits = await uow.get_for(Habit).find_all_generic( + parent_ref_id=habit_collection.ref_id, + allow_archived=JupiterArchivalReason.GC, + ref_id=list(time_events_in_day_for_habits.keys()), + ) + habit_entries = [ + HabitEntry( + habit=habit, + time_events=time_events_in_day_for_habits[habit.ref_id], + ) + for habit in habits + ] + + time_events_in_day_for_chores = _time_events_in_day_grouped_by_owner_ref_id( + time_events_in_day, + NamedEntityTag.CHORE.value, + ) + chores: list[Chore] = [] + if len(time_events_in_day_for_chores) > 0: + chore_collection = await uow.get_for(ChoreCollection).load_by_parent( + workspace.ref_id, + ) + chores = await uow.get_for(Chore).find_all_generic( + parent_ref_id=chore_collection.ref_id, + allow_archived=JupiterArchivalReason.GC, + ref_id=list(time_events_in_day_for_chores.keys()), + ) + chore_entries = [ + ChoreEntry( + chore=chore, + time_events=time_events_in_day_for_chores[chore.ref_id], + ) + for chore in chores + ] + + time_events_in_day_for_activities = _time_events_in_day_grouped_by_owner_ref_id( + time_events_in_day, + NamedEntityTag.TIME_PLAN_ACTIVITY.value, + ) + time_plan_activities: list[TimePlanActivity] = [] + if len(time_events_in_day_for_activities) > 0: + time_plan_activities = await uow.get_for(TimePlanActivity).find_all_generic( + parent_ref_id=None, + allow_archived=True, + ref_id=list(time_events_in_day_for_activities.keys()), + ) + + activity_target_inbox_task_ref_ids = [ + a.target.ref_id for a in time_plan_activities if a.is_target_inbox_task + ] + activity_target_inbox_tasks_by_id: dict[EntityId, InboxTask] = {} + if activity_target_inbox_task_ref_ids: + activity_target_inbox_tasks = await uow.get_for(InboxTask).find_all_generic( + parent_ref_id=None, + allow_archived=True, + ref_id=activity_target_inbox_task_ref_ids, + ) + activity_target_inbox_tasks_by_id = { + it.ref_id: it for it in activity_target_inbox_tasks + } + + activity_target_big_plan_ref_ids = [ + a.target.ref_id for a in time_plan_activities if a.is_target_big_plan + ] + activity_target_big_plans_by_id: dict[EntityId, BigPlan] = {} + if activity_target_big_plan_ref_ids: + activity_target_big_plans = await uow.get_for(BigPlan).find_all_generic( + parent_ref_id=None, + allow_archived=True, + ref_id=activity_target_big_plan_ref_ids, + ) + activity_target_big_plans_by_id = { + bp.ref_id: bp for bp in activity_target_big_plans + } + + time_plan_activity_entries = [ + TimePlanActivityEntry( + time_plan_activity=activity, + target_inbox_task=activity_target_inbox_tasks_by_id.get( + activity.target.ref_id + ), + target_big_plan=activity_target_big_plans_by_id.get( + activity.target.ref_id + ), + time_events=time_events_in_day_for_activities[activity.ref_id], + ) + for activity in time_plan_activities + ] + + time_events_full_days_for_occasions: dict[EntityId, TimeEventFullDaysBlock] = { + te.owner.ref_id: te + for te in time_events_full_days + if te.owner.the_type == NamedEntityTag.OCCASION.value + } + persons = [] + persons_by_ref_id: dict[EntityId, Person] = {} + occasions = [] + contact_domain = None + contact_links_by_person: dict[EntityId, ContactLink] = {} + if len(time_events_full_days_for_occasions) > 0: + prm = await uow.get_for(PRM).load_by_parent( + workspace.ref_id, + ) + persons = await uow.get_for(Person).find_all( + parent_ref_id=prm.ref_id, + allow_archived=True, + ) + + persons_by_ref_id = {p.ref_id: p for p in persons} + occasions = await uow.get_for(Occasion).find_all_generic( + parent_ref_id=None, + allow_archived=True, + ref_id=list(time_events_full_days_for_occasions.keys()), + ) + + # Load contact domain and links for persons + contact_domain = await uow.get_for(ContactDomain).load_by_parent( + workspace.ref_id, + ) + contact_links = await uow.get_for(ContactLink).find_all_generic( + parent_ref_id=contact_domain.ref_id, + allow_archived=False, + owner=[ + EntityLink.std(NamedEntityTag.PERSON.value, p.ref_id) + for p in persons + ], + ) + for link in contact_links: + contact_links_by_person[link.owner.ref_id] = link + + person_occasion_entries = [] + for occasion in occasions: + person = persons_by_ref_id[occasion.person.ref_id] + contact_link = contact_links_by_person.get(person.ref_id) + if contact_link and contact_link.contacts_ref_ids: + # Use the first contact associated with this person + # In a real scenario, you might want a more sophisticated selection + contact_ref_id = contact_link.contacts_ref_ids[0] + contact = await uow.get_for(Contact).load_by_id(contact_ref_id) + person_occasion_entries.append( + PersonOccasionEntry( + contact=contact, + occasion=occasion, + occasion_time_event=time_events_full_days_for_occasions[ + occasion.ref_id + ], + ) + ) + + time_event_full_days_for_vacations: dict[EntityId, TimeEventFullDaysBlock] = { + te.owner.ref_id: te + for te in time_events_full_days + if te.owner.the_type == NamedEntityTag.VACATION.value + } + vacations = [] + if len(time_event_full_days_for_vacations) > 0: + vacation_collection = await uow.get_for(VacationCollection).load_by_parent( + workspace.ref_id, + ) + vacations = await uow.get_for(Vacation).find_all_generic( + parent_ref_id=vacation_collection.ref_id, + allow_archived=False, + ref_id=list(time_event_full_days_for_vacations.keys()), + ) + vacation_entries = [ + VacationEntry( + vacation=vacation, + time_event=time_event_full_days_for_vacations[vacation.ref_id], + ) + for vacation in vacations + ] + + entries = CalendarEventsEntries( + schedule_event_full_days_entries=schedule_event_full_days_entries, + schedule_event_in_day_entries=schedule_event_in_day_entries, + big_plan_entries=big_plan_entries, + todo_task_entries=todo_task_entries, + habit_entries=habit_entries, + chore_entries=chore_entries, + time_plan_activity_entries=time_plan_activity_entries, + person_occasion_entries=person_occasion_entries, + vacation_entries=vacation_entries, + ) + + return entries + + async def build_stats( + self, + uow: DomainUnitOfWork, + schedule: schedules.Schedule, + stats_subperiod: RecurringTaskPeriod, + time_event_domain: TimeEventDomain, + schedule_domain: ScheduleDomain | None = None, + schedule_stream_ref_id: EntityId | None = None, + ) -> CalendarEventsStats: + if schedule_stream_ref_id is not None: + if schedule_domain is None: + raise InputValidationError( + "schedule_domain is required when filtering by schedule stream." + ) + return await self._build_stats_for_schedule_stream( + uow, + schedule, + stats_subperiod, + time_event_domain, + schedule_domain, + schedule_stream_ref_id, + ) + + full_days_raw_stats = await uow.get( + TimeEventFullDaysBlockRepository + ).stats_for_all_between( + parent_ref_id=time_event_domain.ref_id, + start_date=schedule.first_day, + end_date=schedule.end_day, + ) + in_day_raw_stats = await uow.get( + TimeEventInDayBlockRepository + ).stats_for_all_between( + parent_ref_id=time_event_domain.ref_id, + start_date=schedule.first_day, + end_date=schedule.end_day, + ) + + per_subperiod = [] + curr_day = schedule.first_day + while curr_day <= schedule.end_day: + subschedule = schedules.get_schedule( + period=stats_subperiod, + right_now=curr_day.to_timestamp_at_start_of_day(), + name=NOT_USED_NAME, + ) + + schedule_event_full_days_cnt = 0 + schedule_event_in_day_cnt = 0 + big_plan_cnt = 0 + todo_task_cnt = 0 + habit_cnt = 0 + chore_cnt = 0 + time_plan_activity_cnt = 0 + person_birthday_cnt = 0 + vacation_cnt = 0 + + # This is O(N*M) with a rather small M, so it's fine. Probably faster due to memory locality boosts. + for full_days_stats in full_days_raw_stats.per_groups: + if ( + full_days_stats.date >= subschedule.first_day + and full_days_stats.date <= subschedule.end_day + ): + if ( + full_days_stats.entity_tag + == NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value + ): + schedule_event_full_days_cnt += full_days_stats.cnt + elif full_days_stats.entity_tag == NamedEntityTag.OCCASION.value: + person_birthday_cnt += full_days_stats.cnt + elif full_days_stats.entity_tag == NamedEntityTag.VACATION.value: + vacation_cnt += full_days_stats.cnt + for in_day_stats in in_day_raw_stats.per_groups: + if ( + in_day_stats.date >= subschedule.first_day + and in_day_stats.date <= subschedule.end_day + ): + if ( + in_day_stats.entity_tag + == NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value + ): + schedule_event_in_day_cnt += in_day_stats.cnt + elif in_day_stats.entity_tag == NamedEntityTag.BIG_PLAN.value: + big_plan_cnt += in_day_stats.cnt + elif in_day_stats.entity_tag == NamedEntityTag.TODO_TASK.value: + todo_task_cnt += in_day_stats.cnt + elif in_day_stats.entity_tag == NamedEntityTag.HABIT.value: + habit_cnt += in_day_stats.cnt + elif in_day_stats.entity_tag == NamedEntityTag.CHORE.value: + chore_cnt += in_day_stats.cnt + elif ( + in_day_stats.entity_tag + == NamedEntityTag.TIME_PLAN_ACTIVITY.value + ): + time_plan_activity_cnt += in_day_stats.cnt + + per_subperiod.append( + CalendarEventsStatsPerSubperiod( + period=stats_subperiod, + period_start_date=curr_day, + schedule_event_full_days_cnt=schedule_event_full_days_cnt, + schedule_event_in_day_cnt=schedule_event_in_day_cnt, + big_plan_cnt=big_plan_cnt, + todo_task_cnt=todo_task_cnt, + habit_cnt=habit_cnt, + chore_cnt=chore_cnt, + time_plan_activity_cnt=time_plan_activity_cnt, + person_birthday_cnt=person_birthday_cnt, + vacation_cnt=vacation_cnt, + ) + ) + + curr_day = subschedule.end_day.add_days(1) + + stats: CalendarEventsStats = CalendarEventsStats( + subperiod=stats_subperiod, + per_subperiod=per_subperiod, + ) + + return stats + + async def _build_stats_for_schedule_stream( + self, + uow: DomainUnitOfWork, + schedule: schedules.Schedule, + stats_subperiod: RecurringTaskPeriod, + time_event_domain: TimeEventDomain, + schedule_domain: ScheduleDomain, + schedule_stream_ref_id: EntityId, + ) -> CalendarEventsStats: + time_events_full_days: list[TimeEventFullDaysBlock] = await uow.get( + TimeEventFullDaysBlockRepository + ).find_all_between( + parent_ref_id=time_event_domain.ref_id, + start_date=schedule.first_day, + end_date=schedule.end_day, + ) + time_events_in_day: list[TimeEventInDayBlock] = await uow.get( + TimeEventInDayBlockRepository + ).find_all_between( + parent_ref_id=time_event_domain.ref_id, + start_date=schedule.first_day.subtract_days(2), + end_date=schedule.end_day, + ) + + time_events_full_days_for_schedule_events: dict[ + EntityId, TimeEventFullDaysBlock + ] = { + te.owner.ref_id: te + for te in time_events_full_days + if te.owner.the_type == NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value + } + stream_full_days_time_events: list[TimeEventFullDaysBlock] = [] + if len(time_events_full_days_for_schedule_events) > 0: + schedule_events_full_days = await uow.get_for( + ScheduleEventFullDays + ).find_all_generic( + parent_ref_id=schedule_domain.ref_id, + allow_archived=False, + ref_id=list(time_events_full_days_for_schedule_events.keys()), + ) + stream_full_days_time_events = [ + time_events_full_days_for_schedule_events[se.ref_id] + for se in schedule_events_full_days + if se.schedule_stream_ref_id == schedule_stream_ref_id + and se.ref_id in time_events_full_days_for_schedule_events + ] + + time_events_in_day_for_schedule_events = ( + _time_events_in_day_for_owner_type_unique( + time_events_in_day, + NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, + ) + ) + stream_in_day_time_events: list[TimeEventInDayBlock] = [] + if len(time_events_in_day_for_schedule_events) > 0: + schedule_events_in_day = await uow.get_for( + ScheduleEventInDay + ).find_all_generic( + parent_ref_id=schedule_domain.ref_id, + allow_archived=False, + ref_id=list(time_events_in_day_for_schedule_events.keys()), + ) + stream_in_day_time_events = [ + time_events_in_day_for_schedule_events[se.ref_id] + for se in schedule_events_in_day + if se.schedule_stream_ref_id == schedule_stream_ref_id + and se.ref_id in time_events_in_day_for_schedule_events + ] + + per_subperiod = [] + curr_day = schedule.first_day + while curr_day <= schedule.end_day: + subschedule = schedules.get_schedule( + period=stats_subperiod, + right_now=curr_day.to_timestamp_at_start_of_day(), + name=NOT_USED_NAME, + ) + + schedule_event_in_day_cnt = sum( + 1 + for te in stream_in_day_time_events + if te.start_date >= subschedule.first_day + and te.start_date <= subschedule.end_day + ) + schedule_event_full_days_cnt = sum( + 1 + for te in stream_full_days_time_events + if te.start_date <= subschedule.end_day + and te.end_date >= subschedule.first_day + ) + + per_subperiod.append( + CalendarEventsStatsPerSubperiod( + period=stats_subperiod, + period_start_date=curr_day, + schedule_event_full_days_cnt=schedule_event_full_days_cnt, + schedule_event_in_day_cnt=schedule_event_in_day_cnt, + big_plan_cnt=0, + todo_task_cnt=0, + habit_cnt=0, + chore_cnt=0, + time_plan_activity_cnt=0, + person_birthday_cnt=0, + vacation_cnt=0, + ) + ) + + curr_day = subschedule.end_day.add_days(1) + + return CalendarEventsStats( + subperiod=stats_subperiod, + per_subperiod=per_subperiod, + ) diff --git a/src/core/jupiter/core/calendar/use_case/load_for_date_and_period.py b/src/core/jupiter/core/calendar/use_case/load_for_date_and_period.py index 70f65d940..3a2f5f6b8 100644 --- a/src/core/jupiter/core/calendar/use_case/load_for_date_and_period.py +++ b/src/core/jupiter/core/calendar/use_case/load_for_date_and_period.py @@ -1,64 +1,34 @@ """Load all the calendar specific entities for a given date and period.""" -from typing import cast - -from jupiter.core.archival_reason import JupiterArchivalReason -from jupiter.core.big_plans.collection import BigPlanCollection from jupiter.core.big_plans.root import BigPlan -from jupiter.core.chores.collection import ChoreCollection from jupiter.core.chores.root import Chore -from jupiter.core.common import schedules from jupiter.core.common.recurring_task_period import RecurringTaskPeriod -from jupiter.core.common.sub.contacts.root import ContactDomain from jupiter.core.common.sub.contacts.sub.contact.root import Contact -from jupiter.core.common.sub.contacts.sub.link.root import ContactLink -from jupiter.core.common.sub.inbox_tasks.collection import InboxTaskCollection -from jupiter.core.common.sub.inbox_tasks.root import ( - InboxTask, - InboxTaskRepository, -) -from jupiter.core.common.sub.tags.root import TagDomain -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.inbox_tasks.root import InboxTask from jupiter.core.common.sub.tags.sub.tag.root import Tag from jupiter.core.common.sub.time_events.domain import TimeEventDomain from jupiter.core.common.sub.time_events.sub.full_days_block.root import ( TimeEventFullDaysBlock, - TimeEventFullDaysBlockRepository, ) from jupiter.core.common.sub.time_events.sub.in_day_block.root import ( TimeEventInDayBlock, - TimeEventInDayBlockRepository, ) from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.habits.collection import HabitCollection from jupiter.core.habits.root import Habit -from jupiter.core.named_entity_tag import NamedEntityTag -from jupiter.core.prm.root import PRM -from jupiter.core.prm.sub.person.root import Person from jupiter.core.prm.sub.person.sub.occasion.root import Occasion from jupiter.core.schedule.domain import ScheduleDomain -from jupiter.core.schedule.sub.event_full_days.root import ( - ScheduleEventFullDays, -) -from jupiter.core.schedule.sub.event_in_day.root import ( - ScheduleEventInDay, -) +from jupiter.core.schedule.sub.event_full_days.root import ScheduleEventFullDays +from jupiter.core.schedule.sub.event_in_day.root import ScheduleEventInDay from jupiter.core.schedule.sub.stream.root import ScheduleStream from jupiter.core.time_plans.sub.activity.root import TimePlanActivity -from jupiter.core.todo.domain import TodoDomain from jupiter.core.todo.root import TodoTask -from jupiter.core.vacations.collection import VacationCollection from jupiter.core.vacations.root import Vacation -from jupiter.core.workspaces.root import Workspace from jupiter.framework.base.adate import ADate from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink -from jupiter.framework.base.entity_name import NOT_USED_NAME -from jupiter.framework.errors import InputValidationError from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import ( readonly_use_case, @@ -216,29 +186,6 @@ class CalendarLoadForDateAndPeriodResult(UseCaseResultBase): stats: CalendarEventsStats | None -def _time_events_in_day_for_owner_type_unique( - time_events_in_day: list[TimeEventInDayBlock], - owner_type: str, -) -> dict[EntityId, TimeEventInDayBlock]: - return { - te.owner.ref_id: te - for te in time_events_in_day - if te.owner.the_type == owner_type - } - - -def _time_events_in_day_grouped_by_owner_ref_id( - time_events_in_day: list[TimeEventInDayBlock], - owner_type: str, -) -> dict[EntityId, list[TimeEventInDayBlock]]: - result: dict[EntityId, list[TimeEventInDayBlock]] = {} - for te in time_events_in_day: - if te.owner.the_type != owner_type: - continue - result.setdefault(te.owner.ref_id, []).append(te) - return result - - @readonly_use_case(WorkspaceFeature.SCHEDULE) class CalendarLoadForDateAndPeriodUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[ @@ -254,35 +201,15 @@ async def _perform_transactional_read( args: CalendarLoadForDateAndPeriodArgs, ) -> CalendarLoadForDateAndPeriodResult: """Execute the action.""" - if args.stats_subperiod is not None: - if args.period is RecurringTaskPeriod.DAILY: - raise InputValidationError( - "Stats subperiod is not allowed for daily period." - ) - elif args.stats_subperiod not in args.period.all_smaller_periods: - raise InputValidationError( - f"Stats subperiod {args.stats_subperiod} is not smaller than period {args.period}." - ) - - workspace = context.workspace - schedule = schedules.get_schedule( - period=args.period, - right_now=args.right_now.to_timestamp_at_start_of_day(), - name=NOT_USED_NAME, + from jupiter.core.calendar.service.load_for_date_and_period import ( + CalendarLoadForDateAndPeriodService, ) - prev_schedule = schedules.get_schedule( - period=args.period, - right_now=schedule.first_day.subtract_days( - 1 - ).to_timestamp_at_start_of_day(), - name=NOT_USED_NAME, - ) - next_schedule = schedules.get_schedule( - period=args.period, - right_now=schedule.end_day.add_days(1).to_timestamp_at_start_of_day(), - name=NOT_USED_NAME, + + CalendarLoadForDateAndPeriodService.validate_stats_subperiod( + args.period, args.stats_subperiod ) + workspace = context.workspace time_event_domain = await uow.get_for(TimeEventDomain).load_by_parent( workspace.ref_id ) @@ -298,551 +225,14 @@ async def _perform_transactional_read( ss.ref_id: ss for ss in schedule_streams } - entries: CalendarEventsEntries | None = None - if ( - args.period is RecurringTaskPeriod.DAILY - or args.period is RecurringTaskPeriod.WEEKLY - ): - entries = await self._build_entries( - uow, - workspace, - schedule, - time_event_domain, - schedule_domain, - schedule_streams_by_ref_id, - ) - - stats: CalendarEventsStats | None = None - if ( - schedule.period != RecurringTaskPeriod.DAILY - and args.stats_subperiod is not None - ): - stats = await self._build_stats( - uow, schedule, args.stats_subperiod, time_event_domain - ) - - return CalendarLoadForDateAndPeriodResult( - right_now=args.right_now, - period=args.period, - stats_subperiod=args.stats_subperiod, - period_start_date=schedule.first_day, - period_end_date=schedule.end_day, - prev_period_start_date=prev_schedule.first_day, - next_period_start_date=next_schedule.first_day, - entries=entries, - stats=stats, - ) - - async def _build_entries( - self, - uow: DomainUnitOfWork, - workspace: Workspace, - schedule: schedules.Schedule, - time_event_domain: TimeEventDomain, - schedule_domain: ScheduleDomain, - schedule_streams_by_ref_id: dict[EntityId, ScheduleStream], - ) -> CalendarEventsEntries: - time_events_full_days: list[TimeEventFullDaysBlock] = await uow.get( - TimeEventFullDaysBlockRepository - ).find_all_between( - parent_ref_id=time_event_domain.ref_id, - start_date=schedule.first_day, - end_date=schedule.end_day, + return await CalendarLoadForDateAndPeriodService().load( + uow, + workspace, + args.right_now, + args.period, + args.stats_subperiod, + time_event_domain, + schedule_domain, + schedule_streams_by_ref_id, + schedule_stream_ref_id=None, ) - - time_events_in_day: list[TimeEventInDayBlock] = await uow.get( - TimeEventInDayBlockRepository - ).find_all_between( - parent_ref_id=time_event_domain.ref_id, - # Events can be at most 48hrs long, and to catch those that start before the period - # but end inside it we have this little approach. - start_date=schedule.first_day.subtract_days(2), - end_date=schedule.end_day, - ) - - time_events_full_days_for_schedule_events_full_days: dict[ - EntityId, TimeEventFullDaysBlock - ] = { - te.owner.ref_id: te - for te in time_events_full_days - if te.owner.the_type == NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value - } - schedule_events_full_days = [] - if len(time_events_full_days_for_schedule_events_full_days) > 0: - schedule_events_full_days = await uow.get_for( - ScheduleEventFullDays - ).find_all_generic( - parent_ref_id=schedule_domain.ref_id, - allow_archived=False, - ref_id=list(time_events_full_days_for_schedule_events_full_days.keys()), - ) - - tags_domain = await uow.get_for(TagDomain).load_by_parent(workspace.ref_id) - - full_days_tags_by_schedule_event_ref_id: dict[EntityId, list[Tag]] = {} - if schedule_events_full_days: - full_days_tag_links = await uow.get(TagLinkRepository).find_all_generic( - parent_ref_id=tags_domain.ref_id, - allow_archived=False, - owner=[ - EntityLink.std( - NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, - se.ref_id, - ) - for se in schedule_events_full_days - ], - ) - all_fd_tag_ref_ids: list[EntityId] = [] - for tl in full_days_tag_links: - all_fd_tag_ref_ids.extend(tl.ref_ids) - if all_fd_tag_ref_ids: - all_full_days_tags = await uow.get_for(Tag).find_all_generic( - parent_ref_id=tags_domain.ref_id, - allow_archived=False, - ref_id=list(set(all_fd_tag_ref_ids)), - ) - all_full_days_tags_by_ref_id = {t.ref_id: t for t in all_full_days_tags} - else: - all_full_days_tags_by_ref_id = {} - for tag_link in full_days_tag_links: - full_days_tags_by_schedule_event_ref_id[ - cast(EntityId, tag_link.owner.ref_id) - ] = [ - all_full_days_tags_by_ref_id[rid] - for rid in tag_link.ref_ids - if rid in all_full_days_tags_by_ref_id - ] - - schedule_event_full_days_entries = [ - ScheduleFullDaysEventEntry( - event=se, - tags=full_days_tags_by_schedule_event_ref_id.get(se.ref_id, []), - time_event=time_events_full_days_for_schedule_events_full_days[ - se.ref_id - ], - stream=schedule_streams_by_ref_id[se.schedule_stream_ref_id], - ) - for se in schedule_events_full_days - ] - - time_events_in_day_for_schedule_events_in_day = ( - _time_events_in_day_for_owner_type_unique( - time_events_in_day, - NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, - ) - ) - schedule_events_in_day = [] - if len(time_events_in_day_for_schedule_events_in_day) > 0: - schedule_events_in_day = await uow.get_for( - ScheduleEventInDay - ).find_all_generic( - parent_ref_id=schedule_domain.ref_id, - allow_archived=False, - ref_id=list(time_events_in_day_for_schedule_events_in_day.keys()), - ) - - in_day_tags_by_schedule_event_ref_id: dict[EntityId, list[Tag]] = {} - if schedule_events_in_day: - in_day_tag_links = await uow.get(TagLinkRepository).find_all_generic( - parent_ref_id=tags_domain.ref_id, - allow_archived=False, - owner=[ - EntityLink.std( - NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, se.ref_id - ) - for se in schedule_events_in_day - ], - ) - all_in_day_tag_ref_ids: list[EntityId] = [] - for tl in in_day_tag_links: - all_in_day_tag_ref_ids.extend(tl.ref_ids) - if all_in_day_tag_ref_ids: - all_in_day_tags = await uow.get_for(Tag).find_all_generic( - parent_ref_id=tags_domain.ref_id, - allow_archived=False, - ref_id=list(set(all_in_day_tag_ref_ids)), - ) - all_in_day_tags_by_ref_id = {t.ref_id: t for t in all_in_day_tags} - else: - all_in_day_tags_by_ref_id = {} - for tag_link in in_day_tag_links: - in_day_tags_by_schedule_event_ref_id[ - cast(EntityId, tag_link.owner.ref_id) - ] = [ - all_in_day_tags_by_ref_id[rid] - for rid in tag_link.ref_ids - if rid in all_in_day_tags_by_ref_id - ] - - schedule_event_in_day_entries = [ - ScheduleInDayEventEntry( - event=se, - tags=in_day_tags_by_schedule_event_ref_id.get(se.ref_id, []), - time_event=time_events_in_day_for_schedule_events_in_day[se.ref_id], - stream=schedule_streams_by_ref_id[se.schedule_stream_ref_id], - ) - for se in schedule_events_in_day - ] - - time_events_in_day_for_big_plans = _time_events_in_day_grouped_by_owner_ref_id( - time_events_in_day, - NamedEntityTag.BIG_PLAN.value, - ) - big_plans: list[BigPlan] = [] - if len(time_events_in_day_for_big_plans) > 0: - big_plan_collection = await uow.get_for(BigPlanCollection).load_by_parent( - workspace.ref_id, - ) - big_plans = await uow.get_for(BigPlan).find_all_generic( - parent_ref_id=big_plan_collection.ref_id, - allow_archived=JupiterArchivalReason.GC, - ref_id=list(time_events_in_day_for_big_plans.keys()), - ) - big_plan_entries = [ - BigPlanEntry( - big_plan=big_plan, - time_events=time_events_in_day_for_big_plans[big_plan.ref_id], - ) - for big_plan in big_plans - ] - - time_events_in_day_for_todo_tasks = _time_events_in_day_grouped_by_owner_ref_id( - time_events_in_day, - NamedEntityTag.TODO_TASK.value, - ) - todo_tasks: list[TodoTask] = [] - todo_task_inbox_tasks: dict[EntityId, InboxTask] = {} - if len(time_events_in_day_for_todo_tasks) > 0: - todo_domain = await uow.get_for(TodoDomain).load_by_parent( - workspace.ref_id, - ) - todo_tasks = await uow.get_for(TodoTask).find_all_generic( - parent_ref_id=todo_domain.ref_id, - allow_archived=JupiterArchivalReason.GC, - ref_id=list(time_events_in_day_for_todo_tasks.keys()), - ) - inbox_task_collection = await uow.get_for( - InboxTaskCollection - ).load_by_parent(workspace.ref_id) - linked_inbox_tasks = await uow.get( - InboxTaskRepository - ).find_all_for_owner_created_desc( - parent_ref_id=inbox_task_collection.ref_id, - owner=[ - EntityLink.std(NamedEntityTag.TODO_TASK.value, tt.ref_id) - for tt in todo_tasks - ], - allow_archived=JupiterArchivalReason.GC, - ) - for it in linked_inbox_tasks: - if it.owner.ref_id not in todo_task_inbox_tasks: - todo_task_inbox_tasks[it.owner.ref_id] = it - todo_task_entries = [ - TodoTaskEntry( - todo_task=todo_task, - inbox_task=todo_task_inbox_tasks[todo_task.ref_id], - time_events=time_events_in_day_for_todo_tasks[todo_task.ref_id], - ) - for todo_task in todo_tasks - if todo_task.ref_id in todo_task_inbox_tasks - ] - - time_events_in_day_for_habits = _time_events_in_day_grouped_by_owner_ref_id( - time_events_in_day, - NamedEntityTag.HABIT.value, - ) - habits: list[Habit] = [] - if len(time_events_in_day_for_habits) > 0: - habit_collection = await uow.get_for(HabitCollection).load_by_parent( - workspace.ref_id, - ) - habits = await uow.get_for(Habit).find_all_generic( - parent_ref_id=habit_collection.ref_id, - allow_archived=JupiterArchivalReason.GC, - ref_id=list(time_events_in_day_for_habits.keys()), - ) - habit_entries = [ - HabitEntry( - habit=habit, - time_events=time_events_in_day_for_habits[habit.ref_id], - ) - for habit in habits - ] - - time_events_in_day_for_chores = _time_events_in_day_grouped_by_owner_ref_id( - time_events_in_day, - NamedEntityTag.CHORE.value, - ) - chores: list[Chore] = [] - if len(time_events_in_day_for_chores) > 0: - chore_collection = await uow.get_for(ChoreCollection).load_by_parent( - workspace.ref_id, - ) - chores = await uow.get_for(Chore).find_all_generic( - parent_ref_id=chore_collection.ref_id, - allow_archived=JupiterArchivalReason.GC, - ref_id=list(time_events_in_day_for_chores.keys()), - ) - chore_entries = [ - ChoreEntry( - chore=chore, - time_events=time_events_in_day_for_chores[chore.ref_id], - ) - for chore in chores - ] - - time_events_in_day_for_activities = _time_events_in_day_grouped_by_owner_ref_id( - time_events_in_day, - NamedEntityTag.TIME_PLAN_ACTIVITY.value, - ) - time_plan_activities: list[TimePlanActivity] = [] - if len(time_events_in_day_for_activities) > 0: - time_plan_activities = await uow.get_for(TimePlanActivity).find_all_generic( - parent_ref_id=None, - allow_archived=True, - ref_id=list(time_events_in_day_for_activities.keys()), - ) - - activity_target_inbox_task_ref_ids = [ - a.target.ref_id for a in time_plan_activities if a.is_target_inbox_task - ] - activity_target_inbox_tasks_by_id: dict[EntityId, InboxTask] = {} - if activity_target_inbox_task_ref_ids: - activity_target_inbox_tasks = await uow.get_for(InboxTask).find_all_generic( - parent_ref_id=None, - allow_archived=True, - ref_id=activity_target_inbox_task_ref_ids, - ) - activity_target_inbox_tasks_by_id = { - it.ref_id: it for it in activity_target_inbox_tasks - } - - activity_target_big_plan_ref_ids = [ - a.target.ref_id for a in time_plan_activities if a.is_target_big_plan - ] - activity_target_big_plans_by_id: dict[EntityId, BigPlan] = {} - if activity_target_big_plan_ref_ids: - activity_target_big_plans = await uow.get_for(BigPlan).find_all_generic( - parent_ref_id=None, - allow_archived=True, - ref_id=activity_target_big_plan_ref_ids, - ) - activity_target_big_plans_by_id = { - bp.ref_id: bp for bp in activity_target_big_plans - } - - time_plan_activity_entries = [ - TimePlanActivityEntry( - time_plan_activity=activity, - target_inbox_task=activity_target_inbox_tasks_by_id.get( - activity.target.ref_id - ), - target_big_plan=activity_target_big_plans_by_id.get( - activity.target.ref_id - ), - time_events=time_events_in_day_for_activities[activity.ref_id], - ) - for activity in time_plan_activities - ] - - time_events_full_days_for_occasions: dict[EntityId, TimeEventFullDaysBlock] = { - te.owner.ref_id: te - for te in time_events_full_days - if te.owner.the_type == NamedEntityTag.OCCASION.value - } - persons = [] - persons_by_ref_id: dict[EntityId, Person] = {} - occasions = [] - contact_domain = None - contact_links_by_person: dict[EntityId, ContactLink] = {} - if len(time_events_full_days_for_occasions) > 0: - prm = await uow.get_for(PRM).load_by_parent( - workspace.ref_id, - ) - persons = await uow.get_for(Person).find_all( - parent_ref_id=prm.ref_id, - allow_archived=True, - ) - - persons_by_ref_id = {p.ref_id: p for p in persons} - occasions = await uow.get_for(Occasion).find_all_generic( - parent_ref_id=None, - allow_archived=True, - ref_id=list(time_events_full_days_for_occasions.keys()), - ) - - # Load contact domain and links for persons - contact_domain = await uow.get_for(ContactDomain).load_by_parent( - workspace.ref_id, - ) - contact_links = await uow.get_for(ContactLink).find_all_generic( - parent_ref_id=contact_domain.ref_id, - allow_archived=False, - owner=[ - EntityLink.std(NamedEntityTag.PERSON.value, p.ref_id) - for p in persons - ], - ) - for link in contact_links: - contact_links_by_person[link.owner.ref_id] = link - - person_occasion_entries = [] - for occasion in occasions: - person = persons_by_ref_id[occasion.person.ref_id] - contact_link = contact_links_by_person.get(person.ref_id) - if contact_link and contact_link.contacts_ref_ids: - # Use the first contact associated with this person - # In a real scenario, you might want a more sophisticated selection - contact_ref_id = contact_link.contacts_ref_ids[0] - contact = await uow.get_for(Contact).load_by_id(contact_ref_id) - person_occasion_entries.append( - PersonOccasionEntry( - contact=contact, - occasion=occasion, - occasion_time_event=time_events_full_days_for_occasions[ - occasion.ref_id - ], - ) - ) - - time_event_full_days_for_vacations: dict[EntityId, TimeEventFullDaysBlock] = { - te.owner.ref_id: te - for te in time_events_full_days - if te.owner.the_type == NamedEntityTag.VACATION.value - } - vacations = [] - if len(time_event_full_days_for_vacations) > 0: - vacation_collection = await uow.get_for(VacationCollection).load_by_parent( - workspace.ref_id, - ) - vacations = await uow.get_for(Vacation).find_all_generic( - parent_ref_id=vacation_collection.ref_id, - allow_archived=False, - ref_id=list(time_event_full_days_for_vacations.keys()), - ) - vacation_entries = [ - VacationEntry( - vacation=vacation, - time_event=time_event_full_days_for_vacations[vacation.ref_id], - ) - for vacation in vacations - ] - - entries = CalendarEventsEntries( - schedule_event_full_days_entries=schedule_event_full_days_entries, - schedule_event_in_day_entries=schedule_event_in_day_entries, - big_plan_entries=big_plan_entries, - todo_task_entries=todo_task_entries, - habit_entries=habit_entries, - chore_entries=chore_entries, - time_plan_activity_entries=time_plan_activity_entries, - person_occasion_entries=person_occasion_entries, - vacation_entries=vacation_entries, - ) - - return entries - - async def _build_stats( - self, - uow: DomainUnitOfWork, - schedule: schedules.Schedule, - stats_subperiod: RecurringTaskPeriod, - time_event_domain: TimeEventDomain, - ) -> CalendarEventsStats: - full_days_raw_stats = await uow.get( - TimeEventFullDaysBlockRepository - ).stats_for_all_between( - parent_ref_id=time_event_domain.ref_id, - start_date=schedule.first_day, - end_date=schedule.end_day, - ) - in_day_raw_stats = await uow.get( - TimeEventInDayBlockRepository - ).stats_for_all_between( - parent_ref_id=time_event_domain.ref_id, - start_date=schedule.first_day, - end_date=schedule.end_day, - ) - - per_subperiod = [] - curr_day = schedule.first_day - while curr_day <= schedule.end_day: - subschedule = schedules.get_schedule( - period=stats_subperiod, - right_now=curr_day.to_timestamp_at_start_of_day(), - name=NOT_USED_NAME, - ) - - schedule_event_full_days_cnt = 0 - schedule_event_in_day_cnt = 0 - big_plan_cnt = 0 - todo_task_cnt = 0 - habit_cnt = 0 - chore_cnt = 0 - time_plan_activity_cnt = 0 - person_birthday_cnt = 0 - vacation_cnt = 0 - - # This is O(N*M) with a rather small M, so it's fine. Probably faster due to memory locality boosts. - for full_days_stats in full_days_raw_stats.per_groups: - if ( - full_days_stats.date >= subschedule.first_day - and full_days_stats.date <= subschedule.end_day - ): - if ( - full_days_stats.entity_tag - == NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value - ): - schedule_event_full_days_cnt += full_days_stats.cnt - elif full_days_stats.entity_tag == NamedEntityTag.OCCASION.value: - person_birthday_cnt += full_days_stats.cnt - elif full_days_stats.entity_tag == NamedEntityTag.VACATION.value: - vacation_cnt += full_days_stats.cnt - for in_day_stats in in_day_raw_stats.per_groups: - if ( - in_day_stats.date >= subschedule.first_day - and in_day_stats.date <= subschedule.end_day - ): - if ( - in_day_stats.entity_tag - == NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value - ): - schedule_event_in_day_cnt += in_day_stats.cnt - elif in_day_stats.entity_tag == NamedEntityTag.BIG_PLAN.value: - big_plan_cnt += in_day_stats.cnt - elif in_day_stats.entity_tag == NamedEntityTag.TODO_TASK.value: - todo_task_cnt += in_day_stats.cnt - elif in_day_stats.entity_tag == NamedEntityTag.HABIT.value: - habit_cnt += in_day_stats.cnt - elif in_day_stats.entity_tag == NamedEntityTag.CHORE.value: - chore_cnt += in_day_stats.cnt - elif ( - in_day_stats.entity_tag - == NamedEntityTag.TIME_PLAN_ACTIVITY.value - ): - time_plan_activity_cnt += in_day_stats.cnt - - per_subperiod.append( - CalendarEventsStatsPerSubperiod( - period=stats_subperiod, - period_start_date=curr_day, - schedule_event_full_days_cnt=schedule_event_full_days_cnt, - schedule_event_in_day_cnt=schedule_event_in_day_cnt, - big_plan_cnt=big_plan_cnt, - todo_task_cnt=todo_task_cnt, - habit_cnt=habit_cnt, - chore_cnt=chore_cnt, - time_plan_activity_cnt=time_plan_activity_cnt, - person_birthday_cnt=person_birthday_cnt, - vacation_cnt=vacation_cnt, - ) - ) - - curr_day = subschedule.end_day.add_days(1) - - stats: CalendarEventsStats = CalendarEventsStats( - subperiod=stats_subperiod, - per_subperiod=per_subperiod, - ) - - return stats diff --git a/src/core/jupiter/core/calendar/use_case/load_public_for_schedule_stream.py b/src/core/jupiter/core/calendar/use_case/load_public_for_schedule_stream.py new file mode 100644 index 000000000..417edd859 --- /dev/null +++ b/src/core/jupiter/core/calendar/use_case/load_public_for_schedule_stream.py @@ -0,0 +1,113 @@ +"""Guest readonly use case for loading calendar data for a published schedule stream.""" + +from jupiter.core.calendar.service.load_for_date_and_period import ( + CalendarLoadForDateAndPeriodService, +) +from jupiter.core.calendar.use_case.load_for_date_and_period import ( + CalendarLoadForDateAndPeriodResult, +) +from jupiter.core.common.recurring_task_period import RecurringTaskPeriod +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.common.sub.time_events.domain import TimeEventDomain +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.schedule.domain import ScheduleDomain +from jupiter.core.schedule.sub.stream.root import ScheduleStream +from jupiter.core.workspaces.root import Workspace +from jupiter.framework.base.adate import ADate +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class CalendarLoadPublicForScheduleStreamArgs(UseCaseArgsBase): + """CalendarLoadPublicForScheduleStream args.""" + + external_id: PublishExternalId + right_now: ADate + period: RecurringTaskPeriod + stats_subperiod: RecurringTaskPeriod | None + + +class CalendarLoadPublicForScheduleStreamUseCase( + JupiterGuestReadonlyUseCase[ + CalendarLoadPublicForScheduleStreamArgs, + CalendarLoadForDateAndPeriodResult, + ] +): + """Load calendar entries and stats for a published schedule stream.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: CalendarLoadPublicForScheduleStreamArgs, + ) -> CalendarLoadForDateAndPeriodResult: + """Execute the use case.""" + CalendarLoadForDateAndPeriodService.validate_stats_subperiod( + args.period, args.stats_subperiod + ) + + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.SCHEDULE_STREAM.value: + raise InputValidationError( + "The publish entity does not refer to a schedule stream." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + schedule_domain = await uow.get_for(ScheduleDomain).load_by_parent( + publish_domain.workspace.ref_id + ) + schedule_stream = await uow.get_for(ScheduleStream).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if schedule_stream.parent_ref_id != schedule_domain.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace schedule stream." + ) + + workspace = await uow.get_for(Workspace).load_by_id( + publish_domain.workspace.ref_id + ) + + time_event_domain = await uow.get_for(TimeEventDomain).load_by_parent( + workspace.ref_id + ) + + schedule_streams_by_ref_id: dict[EntityId, ScheduleStream] = { + schedule_stream.ref_id: schedule_stream + } + + return await CalendarLoadForDateAndPeriodService().load( + uow, + workspace, + args.right_now, + args.period, + args.stats_subperiod, + time_event_domain, + schedule_domain, + schedule_streams_by_ref_id, + schedule_stream_ref_id=schedule_stream.ref_id, + ) diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index 85db38b8e..0b52001da 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -28,7 +28,7 @@ { NamedEntityTag.TODO_TASK.value, # done NamedEntityTag.TIME_PLAN.value, # done - NamedEntityTag.SCHEDULE_STREAM.value, + NamedEntityTag.SCHEDULE_STREAM.value, # done NamedEntityTag.SCHEDULE_EVENT_IN_DAY.value, # done NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS_BLOCK.value, # done NamedEntityTag.HABIT.value, # done diff --git a/src/core/jupiter/core/schedule/sub/event_full_days/use_case/load_public_from_schedule_stream.py b/src/core/jupiter/core/schedule/sub/event_full_days/use_case/load_public_from_schedule_stream.py new file mode 100644 index 000000000..7f39349c3 --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/event_full_days/use_case/load_public_from_schedule_stream.py @@ -0,0 +1,104 @@ +"""Guest readonly use case for loading a schedule full days event via a published stream.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.schedule.domain import ScheduleDomain +from jupiter.core.schedule.sub.event_full_days.root import ScheduleEventFullDays +from jupiter.core.schedule.sub.event_full_days.service.load import ( + ScheduleEventFullDaysLoadResult, + ScheduleEventFullDaysLoadService, +) +from jupiter.core.schedule.sub.stream.root import ScheduleStream +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs(UseCaseArgsBase): + """ScheduleEventFullDaysLoadPublicFromScheduleStream args.""" + + external_id: PublishExternalId + ref_id: EntityId + + +class ScheduleEventFullDaysLoadPublicFromScheduleStreamUseCase( + JupiterGuestReadonlyUseCase[ + ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs, + ScheduleEventFullDaysLoadResult, + ] +): + """Load a schedule full days event through a published schedule stream.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: ScheduleEventFullDaysLoadPublicFromScheduleStreamArgs, + ) -> ScheduleEventFullDaysLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.SCHEDULE_STREAM.value: + raise InputValidationError( + "The publish entity does not refer to a schedule stream." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + schedule_domain = await uow.get_for(ScheduleDomain).load_by_parent( + publish_domain.workspace.ref_id + ) + schedule_stream = await uow.get_for(ScheduleStream).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if schedule_stream.parent_ref_id != schedule_domain.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace schedule stream." + ) + + schedule_event_full_days = await uow.get_for( + ScheduleEventFullDays + ).load_by_id( + args.ref_id, + allow_archived=False, + ) + if schedule_event_full_days.parent_ref_id != schedule_domain.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace schedule event." + ) + if ( + schedule_event_full_days.schedule_stream_ref_id + != schedule_stream.ref_id + ): + raise InputValidationError( + "The schedule event does not belong to the published schedule stream." + ) + + return await ScheduleEventFullDaysLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + schedule_event_full_days, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public_from_schedule_stream.py b/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public_from_schedule_stream.py new file mode 100644 index 000000000..7ed59c3c6 --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/event_in_day/use_case/load_public_from_schedule_stream.py @@ -0,0 +1,99 @@ +"""Guest readonly use case for loading a schedule event in day via a published stream.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.schedule.domain import ScheduleDomain +from jupiter.core.schedule.sub.event_in_day.root import ScheduleEventInDay +from jupiter.core.schedule.sub.event_in_day.service.load import ( + ScheduleEventInDayLoadResult, + ScheduleEventInDayLoadService, +) +from jupiter.core.schedule.sub.stream.root import ScheduleStream +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class ScheduleEventInDayLoadPublicFromScheduleStreamArgs(UseCaseArgsBase): + """ScheduleEventInDayLoadPublicFromScheduleStream args.""" + + external_id: PublishExternalId + ref_id: EntityId + + +class ScheduleEventInDayLoadPublicFromScheduleStreamUseCase( + JupiterGuestReadonlyUseCase[ + ScheduleEventInDayLoadPublicFromScheduleStreamArgs, + ScheduleEventInDayLoadResult, + ] +): + """Load a schedule event in day through a published schedule stream.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: ScheduleEventInDayLoadPublicFromScheduleStreamArgs, + ) -> ScheduleEventInDayLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.SCHEDULE_STREAM.value: + raise InputValidationError( + "The publish entity does not refer to a schedule stream." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + schedule_domain = await uow.get_for(ScheduleDomain).load_by_parent( + publish_domain.workspace.ref_id + ) + schedule_stream = await uow.get_for(ScheduleStream).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if schedule_stream.parent_ref_id != schedule_domain.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace schedule stream." + ) + + schedule_event_in_day = await uow.get_for(ScheduleEventInDay).load_by_id( + args.ref_id, + allow_archived=False, + ) + if schedule_event_in_day.parent_ref_id != schedule_domain.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace schedule event." + ) + if schedule_event_in_day.schedule_stream_ref_id != schedule_stream.ref_id: + raise InputValidationError( + "The schedule event does not belong to the published schedule stream." + ) + + return await ScheduleEventInDayLoadService().do_it( + uow, + publish_domain.workspace.ref_id, + schedule_event_in_day, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/schedule/sub/stream/root.py b/src/core/jupiter/core/schedule/sub/stream/root.py index 8e0518a8a..74d9a6c0f 100644 --- a/src/core/jupiter/core/schedule/sub/stream/root.py +++ b/src/core/jupiter/core/schedule/sub/stream/root.py @@ -1,6 +1,7 @@ """A specific schedule group or stream of events.""" from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.link.root import TagLink from jupiter.core.common.url import URL from jupiter.core.named_entity_tag import NamedEntityTag @@ -56,6 +57,9 @@ class ScheduleStream(LeafEntity): note = OwnsAtMostOne( Note, owner=IsEntityLinkStd(NamedEntityTag.SCHEDULE_STREAM.value) ) + publish_entity = OwnsAtMostOne( + PublishEntity, owner=IsEntityLinkStd(NamedEntityTag.SCHEDULE_STREAM.value) + ) @staticmethod @create_entity_action diff --git a/src/core/jupiter/core/schedule/sub/stream/service/__init__.py b/src/core/jupiter/core/schedule/sub/stream/service/__init__.py new file mode 100644 index 000000000..1a321491d --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/stream/service/__init__.py @@ -0,0 +1 @@ +"""Schedule stream service.""" diff --git a/src/core/jupiter/core/schedule/sub/stream/service/load.py b/src/core/jupiter/core/schedule/sub/stream/service/load.py new file mode 100644 index 000000000..353942649 --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/stream/service/load.py @@ -0,0 +1,81 @@ +"""Shared service for loading a schedule stream.""" + +from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.schedule.sub.stream.root import ScheduleStream +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.storage.repository import DomainUnitOfWork +from jupiter.framework.use_case_io import UseCaseResultBase, use_case_result + + +@use_case_result +class ScheduleStreamLoadResult(UseCaseResultBase): + """ScheduleStreamLoadResult.""" + + schedule_stream: ScheduleStream + note: Note | None + tags: list[Tag] + publish_entity: PublishEntity | None + + +class ScheduleStreamLoadService: + """Shared service for loading a schedule stream.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + schedule_stream: ScheduleStream, + *, + allow_archived: bool = False, + include_publish_entity: bool = True, + ) -> ScheduleStreamLoadResult: + """Load a schedule stream and its dependent entities.""" + schedule_stream = await uow.get_for(ScheduleStream).load_by_id( + schedule_stream.ref_id, + allow_archived=allow_archived, + ) + + note = await uow.get(NoteRepository).load_optional_for_owner( + EntityLink.std( + NamedEntityTag.SCHEDULE_STREAM.value, schedule_stream.ref_id + ), + allow_archived=allow_archived, + ) + + tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( + owner=EntityLink.std( + NamedEntityTag.SCHEDULE_STREAM.value, schedule_stream.ref_id + ), + ) + if tag_link is not None: + tags = await uow.get(TagRepository).find_all_generic( + parent_ref_id=tag_link.tag_domain.ref_id, + allow_archived=False, + ref_id=tag_link.ref_ids, + ) + else: + tags = [] + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std( + NamedEntityTag.SCHEDULE_STREAM.value, schedule_stream.ref_id + ), + allow_archived=allow_archived, + ) + + return ScheduleStreamLoadResult( + schedule_stream=schedule_stream, + note=note, + tags=tags, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/schedule/sub/stream/use_case/load.py b/src/core/jupiter/core/schedule/sub/stream/use_case/load.py index d99eee91b..2fb709614 100644 --- a/src/core/jupiter/core/schedule/sub/stream/use_case/load.py +++ b/src/core/jupiter/core/schedule/sub/stream/use_case/load.py @@ -1,28 +1,31 @@ """Use case for loading a particular stream.""" -from jupiter.core.common.sub.notes.root import Note, NoteRepository -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository -from jupiter.core.common.sub.tags.sub.tag.root import Tag, TagRepository from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, ) from jupiter.core.features import WorkspaceFeature -from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.core.schedule.sub.stream.root import ScheduleStream +from jupiter.core.schedule.sub.stream.service.load import ( + ScheduleStreamLoadResult, + ScheduleStreamLoadService, +) from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import ( readonly_use_case, ) from jupiter.framework.use_case_io import ( UseCaseArgsBase, - UseCaseResultBase, use_case_args, - use_case_result, ) +__all__ = [ + "ScheduleStreamLoadArgs", + "ScheduleStreamLoadResult", + "ScheduleStreamLoadUseCase", +] + @use_case_args class ScheduleStreamLoadArgs(UseCaseArgsBase): @@ -32,15 +35,6 @@ class ScheduleStreamLoadArgs(UseCaseArgsBase): allow_archived: bool | None -@use_case_result -class ScheduleStreamLoadResult(UseCaseResultBase): - """Result.""" - - schedule_stream: ScheduleStream - note: Note | None - tags: list[Tag] - - @readonly_use_case(WorkspaceFeature.SCHEDULE) class ScheduleStreamLoadUseCase( JupiterTransactionalLoggedInReadOnlyUseCase[ @@ -61,27 +55,9 @@ async def _perform_transactional_read( args.ref_id, allow_archived=allow_archived ) - note = await uow.get(NoteRepository).load_optional_for_owner( - EntityLink.std( - NamedEntityTag.SCHEDULE_STREAM.value, schedule_stream.ref_id - ), + return await ScheduleStreamLoadService().do_it( + uow, + schedule_stream, allow_archived=allow_archived, - ) - - tag_link = await uow.get(TagLinkRepository).load_optional_for_owner( - owner=EntityLink.std( - NamedEntityTag.SCHEDULE_STREAM.value, schedule_stream.ref_id - ), - ) - if tag_link is not None: - tags = await uow.get(TagRepository).find_all_generic( - parent_ref_id=tag_link.tag_domain.ref_id, - allow_archived=False, - ref_id=tag_link.ref_ids, - ) - else: - tags = [] - - return ScheduleStreamLoadResult( - schedule_stream=schedule_stream, note=note, tags=tags + include_publish_entity=True, ) diff --git a/src/core/jupiter/core/schedule/sub/stream/use_case/load_public.py b/src/core/jupiter/core/schedule/sub/stream/use_case/load_public.py new file mode 100644 index 000000000..91d737141 --- /dev/null +++ b/src/core/jupiter/core/schedule/sub/stream/use_case/load_public.py @@ -0,0 +1,79 @@ +"""Guest readonly use case for loading a published schedule stream.""" + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.core.schedule.domain import ScheduleDomain +from jupiter.core.schedule.sub.stream.root import ScheduleStream +from jupiter.core.schedule.sub.stream.service.load import ( + ScheduleStreamLoadResult, + ScheduleStreamLoadService, +) +from jupiter.framework.errors import InputValidationError +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class ScheduleStreamLoadPublicArgs(UseCaseArgsBase): + """ScheduleStreamLoadPublic args.""" + + external_id: PublishExternalId + + +class ScheduleStreamLoadPublicUseCase( + JupiterGuestReadonlyUseCase[ScheduleStreamLoadPublicArgs, ScheduleStreamLoadResult] +): + """Load a published schedule stream by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: ScheduleStreamLoadPublicArgs, + ) -> ScheduleStreamLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + args.external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.SCHEDULE_STREAM.value: + raise InputValidationError( + "The publish entity does not refer to a schedule stream." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + schedule_domain = await uow.get_for(ScheduleDomain).load_by_parent( + publish_domain.workspace.ref_id + ) + schedule_stream = await uow.get_for(ScheduleStream).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if schedule_stream.parent_ref_id != schedule_domain.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace schedule stream." + ) + + return await ScheduleStreamLoadService().do_it( + uow, + schedule_stream, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx index 7e170c664..12ec720b0 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -48,6 +48,8 @@ function publishedEntityLocation(externalId: string, owner: string): string { return `/app/public/published/chore/${externalId}`; case NamedEntityTag.BIG_PLAN: return `/app/public/published/big-plan/${externalId}`; + case NamedEntityTag.SCHEDULE_STREAM: + return `/app/public/published/schedule-stream/${externalId}`; default: throw new Response(ReasonPhrases.NOT_FOUND, { status: StatusCodes.NOT_FOUND, diff --git a/src/webui/app/routes/app/public/published/schedule-stream/$externalId.tsx b/src/webui/app/routes/app/public/published/schedule-stream/$externalId.tsx new file mode 100644 index 000000000..c29864744 --- /dev/null +++ b/src/webui/app/routes/app/public/published/schedule-stream/$externalId.tsx @@ -0,0 +1,310 @@ +import { AppPlatform, RecurringTaskPeriod } from "@jupiter/webapi-client"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json, redirect } from "@remix-run/node"; +import type { ShouldRevalidateFunction } from "@remix-run/react"; +import { Outlet, useSearchParams } from "@remix-run/react"; +import { AnimatePresence } from "framer-motion"; +import { DateTime } from "luxon"; +import { useContext, useEffect, useMemo, useState } from "react"; +import { z } from "zod"; +import { parseParams, parseQuery } from "zodix"; +import { periodName } from "@jupiter/core/common/recurring-task-period"; +import { + CalendarNavigationProvider, + publishedScheduleStreamCalendarNavigation, +} from "@jupiter/core/calendar/component/calendar-navigation"; +import { View } from "@jupiter/core/calendar/component/shared"; +import { ViewAsCalendarDaily } from "@jupiter/core/calendar/component/view-as-calendar-daily"; +import { ViewAsCalendarWeekly } from "@jupiter/core/calendar/component/view-as-calendar-weekly"; +import { ViewAsScheduleDailyAndWeekly } from "@jupiter/core/calendar/component/view-as-schedule-daily-and-weekly"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { NestingAwareBlock } from "@jupiter/core/infra/component/layout/nesting-aware-block"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { + NavMultipleCompact, + NavMultipleSpread, + NavSingle, + SectionActions, +} from "@jupiter/core/infra/component/section-actions"; +import { + DisplayType, + useLeafNeedsToShowLeaflet, +} from "@jupiter/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { inferPlatformAndDistribution } from "@jupiter/core/frontdoor.server"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; +import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; +import { newURLParams } from "~/logic/navigation"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +const QuerySchema = z.object({ + date: z + .string() + .regex(/[0-9][0-9][0-9][0-9][-][0-9][0-9][-][0-9][0-9]/) + .optional(), + period: z.nativeEnum(RecurringTaskPeriod).optional(), + view: z.nativeEnum(View).optional(), +}); + +const ALLOWED_PERIODS = [ + RecurringTaskPeriod.DAILY, + RecurringTaskPeriod.WEEKLY, +] as const; + +function defaultPeriodForPlatform(platform: AppPlatform): RecurringTaskPeriod { + return platform === AppPlatform.MOBILE_IOS || + platform === AppPlatform.MOBILE_ANDROID + ? RecurringTaskPeriod.DAILY + : RecurringTaskPeriod.WEEKLY; +} + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + const { externalId } = parseParams(params, ParamsSchema); + const query = parseQuery(request, QuerySchema); + const url = new URL(request.url); + + const { platform } = inferPlatformAndDistribution( + request.headers.get("User-Agent"), + ); + + const defaultPeriod = defaultPeriodForPlatform(platform); + + if ( + query.date === undefined || + query.period === undefined || + query.view === undefined || + (query.period !== undefined && + !ALLOWED_PERIODS.includes( + query.period as (typeof ALLOWED_PERIODS)[number], + )) + ) { + url.searchParams.set("date", query.date || DateTime.now().toISODate()); + url.searchParams.set( + "period", + query.period && + ALLOWED_PERIODS.includes( + query.period as (typeof ALLOWED_PERIODS)[number], + ) + ? query.period + : defaultPeriod, + ); + url.searchParams.set("view", query.view || View.CALENDAR); + + return redirect(url.pathname + url.search); + } + + const apiClient = await getGuestApiClient(request); + + const [streamResponse, calendarResponse] = await Promise.all([ + apiClient.schedule.scheduleStreamLoadPublic({ + external_id: externalId, + }), + apiClient.calendar.calendarLoadPublicForScheduleStream({ + external_id: externalId, + right_now: query.date, + period: query.period, + stats_subperiod: null, + }), + ]); + + return json({ + externalId, + scheduleStream: streamResponse.schedule_stream, + date: query.date as string, + period: query.period as RecurringTaskPeriod, + view: query.view as View, + periodStartDate: calendarResponse.period_start_date, + periodEndDate: calendarResponse.period_end_date, + prevPeriodStartDate: calendarResponse.prev_period_start_date, + nextPeriodStartDate: calendarResponse.next_period_start_date, + entries: calendarResponse.entries || undefined, + stats: calendarResponse.stats || undefined, + }); + } catch (error) { + handlePublishedLoaderError(error); + } +} + +export const shouldRevalidate: ShouldRevalidateFunction = + standardShouldRevalidate; + +const REFRESH_RIGHT_NOW_MS = 1000 * 60 * 5; + +export default function PublishedScheduleStream() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const shouldShowALeaflet = useLeafNeedsToShowLeaflet(); + const [query] = useSearchParams(); + + const calendarLocation = ""; + const isAdding = false; + const basePath = `/app/public/published/schedule-stream/${loaderData.externalId}`; + + const calendarNavigation = useMemo( + () => publishedScheduleStreamCalendarNavigation(loaderData.externalId), + [loaderData.externalId], + ); + + const [rightNow, setRightNow] = useState( + DateTime.local({ zone: topLevelInfo.user.timezone }), + ); + const theRealToday = rightNow.toISODate()!; + + useEffect(() => { + const timeout = setInterval(() => { + setRightNow(DateTime.local({ zone: topLevelInfo.user.timezone })); + }, REFRESH_RIGHT_NOW_MS); + + return () => { + clearInterval(timeout); + }; + }, [topLevelInfo.user.timezone]); + + const viewProps = { + rightNow, + timezone: topLevelInfo.user.timezone, + today: theRealToday, + period: loaderData.period, + periodStartDate: loaderData.periodStartDate, + periodEndDate: loaderData.periodEndDate, + entries: loaderData.entries, + stats: loaderData.stats, + calendarLocation, + isAdding, + }; + + const calendarActions = ( + <SectionActions + id="published-schedule-stream" + topLevelInfo={topLevelInfo} + inputsEnabled={true} + actions={[ + NavMultipleSpread({ + navs: [ + NavSingle({ + text: "Today", + link: `${basePath}?${newURLParams(query, "date", theRealToday)}`, + }), + NavSingle({ + text: "Prev", + icon: <ArrowBackIcon />, + link: `${basePath}?${newURLParams( + query, + "date", + loaderData.prevPeriodStartDate, + )}`, + }), + NavSingle({ + text: "Next", + icon: <ArrowForwardIcon />, + link: `${basePath}?${newURLParams( + query, + "date", + loaderData.nextPeriodStartDate, + )}`, + }), + ], + }), + NavMultipleCompact({ + navs: [ + NavSingle({ + text: periodName(RecurringTaskPeriod.DAILY), + highlight: loaderData.period === RecurringTaskPeriod.DAILY, + link: `${basePath}?${newURLParams( + query, + "period", + RecurringTaskPeriod.DAILY, + )}`, + }), + NavSingle({ + text: periodName(RecurringTaskPeriod.WEEKLY), + highlight: loaderData.period === RecurringTaskPeriod.WEEKLY, + link: `${basePath}?${newURLParams( + query, + "period", + RecurringTaskPeriod.WEEKLY, + )}`, + }), + ], + }), + NavMultipleCompact({ + navs: [ + NavSingle({ + text: "Calendar", + link: `${basePath}?${newURLParams(query, "view", View.CALENDAR)}`, + highlight: loaderData.view === View.CALENDAR, + }), + NavSingle({ + text: "Schedule", + link: `${basePath}?${newURLParams(query, "view", View.SCHEDULE)}`, + highlight: loaderData.view === View.SCHEDULE, + }), + ], + }), + ]} + /> + ); + + return ( + <CalendarNavigationProvider value={calendarNavigation}> + <LeafPanel + key={`published-schedule-stream-${loaderData.externalId}`} + fakeKey={`published-schedule-stream-${loaderData.externalId}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation="/app" + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + shouldShowALeaflet={shouldShowALeaflet} + > + <NestingAwareBlock shouldHide={shouldShowALeaflet}> + <SectionCard + title={loaderData.scheduleStream.name} + actions={calendarActions} + > + {loaderData.view === View.CALENDAR && + loaderData.period === RecurringTaskPeriod.DAILY && ( + <ViewAsCalendarDaily {...viewProps} /> + )} + + {loaderData.view === View.CALENDAR && + loaderData.period === RecurringTaskPeriod.WEEKLY && ( + <ViewAsCalendarWeekly {...viewProps} /> + )} + + {loaderData.view === View.SCHEDULE && ( + <ViewAsScheduleDailyAndWeekly {...viewProps} /> + )} + </SectionCard> + </NestingAwareBlock> + + <AnimatePresence mode="wait" initial={false}> + <Outlet /> + </AnimatePresence> + </LeafPanel> + </CalendarNavigationProvider> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => + `Could not find published schedule stream ${params.externalId}!`, + error: (params) => + `There was an error loading published schedule stream ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/public/published/schedule-stream/$externalId/full-days-event/$eventId.tsx b/src/webui/app/routes/app/public/published/schedule-stream/$externalId/full-days-event/$eventId.tsx new file mode 100644 index 000000000..152f58c80 --- /dev/null +++ b/src/webui/app/routes/app/public/published/schedule-stream/$externalId/full-days-event/$eventId.tsx @@ -0,0 +1,111 @@ +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { useSearchParams } from "@remix-run/react"; +import { useContext } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { ScheduleEventFullDaysEditor } from "@jupiter/core/schedule/sub/event_full_days/component/editor"; +import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_full_days/root"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), + eventId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAFLET, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + const { externalId, eventId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + const result = + await apiClient.schedule.scheduleEventFullDaysLoadPublicFromScheduleStream( + { + external_id: externalId, + ref_id: eventId, + }, + ); + + return json({ + externalId, + scheduleEventFullDays: result.schedule_event_full_days, + timeEventFullDaysBlock: result.time_event_full_days_block, + scheduleStream: result.schedule_stream, + note: result.note ?? null, + tags: result.tags ?? [], + contacts: result.contacts ?? [], + }); + } catch (error) { + handlePublishedLoaderError(error); + } +} + +export default function PublishedScheduleStreamFullDaysEvent() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const [query] = useSearchParams(); + + return ( + <LeafPanel + key={`published-schedule-event-full-days-${loaderData.scheduleEventFullDays.ref_id}`} + fakeKey={`published-schedule-event-full-days-${loaderData.scheduleEventFullDays.ref_id}`} + isLeaflet + inputsEnabled={false} + entityNotEditable={true} + returnLocation={`/app/public/published/schedule-stream/${loaderData.externalId}?${query}`} + > + <ScheduleEventFullDaysEditor + scheduleEventFullDays={loaderData.scheduleEventFullDays} + timeEventFullDaysBlock={loaderData.timeEventFullDaysBlock} + allScheduleStreams={[loaderData.scheduleStream]} + tags={loaderData.tags} + contacts={loaderData.contacts} + allTags={loaderData.tags} + allContacts={loaderData.contacts} + inputsEnabled={false} + corePropertyEditable={isCorePropertyEditable( + loaderData.scheduleEventFullDays, + )} + topLevelInfo={topLevelInfo} + /> + + <SectionCard title="Note"> + {loaderData.note ? ( + <EntityNoteEditor + initialNote={loaderData.note} + inputsEnabled={false} + /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary( + (params) => `/app/public/published/schedule-stream/${params.externalId}`, + ParamsSchema, + { + notFound: (params) => + `Could not find full-days event ${params.eventId} in published stream ${params.externalId}!`, + error: (params) => + `There was an error loading full-days event ${params.eventId}! Please try again!`, + }, +); diff --git a/src/webui/app/routes/app/public/published/schedule-stream/$externalId/in-day-event/$eventId.tsx b/src/webui/app/routes/app/public/published/schedule-stream/$externalId/in-day-event/$eventId.tsx new file mode 100644 index 000000000..d4bda13c7 --- /dev/null +++ b/src/webui/app/routes/app/public/published/schedule-stream/$externalId/in-day-event/$eventId.tsx @@ -0,0 +1,125 @@ +import { Typography } from "@mui/material"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { useSearchParams } from "@remix-run/react"; +import { useContext, useMemo } from "react"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { timeEventInDayBlockParamsToTimezone } from "@jupiter/core/common/sub/time_events/time-event"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; +import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { ScheduleEventInDayEditor } from "@jupiter/core/schedule/sub/event_in_day/component/editor"; +import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_in_day/root"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), + eventId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAFLET, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + const { externalId, eventId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + const result = + await apiClient.schedule.scheduleEventInDayLoadPublicFromScheduleStream({ + external_id: externalId, + ref_id: eventId, + }); + + return json({ + externalId, + scheduleEventInDay: result.schedule_event_in_day, + timeEventInDayBlock: result.time_event_in_day_block, + scheduleStream: result.schedule_stream, + note: result.note ?? null, + tags: result.tags ?? [], + contacts: result.contacts ?? [], + }); + } catch (error) { + handlePublishedLoaderError(error); + } +} + +export default function PublishedScheduleStreamInDayEvent() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const topLevelInfo = useContext(TopLevelInfoContext); + const [query] = useSearchParams(); + + const blockParamsInTz = useMemo( + () => + timeEventInDayBlockParamsToTimezone( + { + startDate: loaderData.timeEventInDayBlock.start_date, + startTimeInDay: loaderData.timeEventInDayBlock.start_time_in_day, + }, + topLevelInfo.user.timezone, + ), + [loaderData.timeEventInDayBlock, topLevelInfo.user.timezone], + ); + + return ( + <LeafPanel + key={`published-schedule-event-in-day-${loaderData.scheduleEventInDay.ref_id}`} + fakeKey={`published-schedule-event-in-day-${loaderData.scheduleEventInDay.ref_id}`} + isLeaflet + inputsEnabled={false} + entityNotEditable={true} + returnLocation={`/app/public/published/schedule-stream/${loaderData.externalId}?${query}`} + > + <ScheduleEventInDayEditor + scheduleEventInDay={loaderData.scheduleEventInDay} + timeEventInDayBlock={loaderData.timeEventInDayBlock} + allScheduleStreams={[loaderData.scheduleStream]} + tags={loaderData.tags} + contacts={loaderData.contacts} + allTags={loaderData.tags} + allContacts={loaderData.contacts} + inputsEnabled={false} + corePropertyEditable={isCorePropertyEditable( + loaderData.scheduleEventInDay, + )} + topLevelInfo={topLevelInfo} + startDate={blockParamsInTz.startDate} + startTimeInDay={blockParamsInTz.startTimeInDay!} + durationMins={loaderData.timeEventInDayBlock.duration_mins} + /> + + <SectionCard title="Note"> + {loaderData.note ? ( + <EntityNoteEditor + initialNote={loaderData.note} + inputsEnabled={false} + /> + ) : ( + <Typography variant="body2" color="text.secondary"> + No note. + </Typography> + )} + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary( + (params) => `/app/public/published/schedule-stream/${params.externalId}`, + ParamsSchema, + { + notFound: (params) => + `Could not find in-day event ${params.eventId} in published stream ${params.externalId}!`, + error: (params) => + `There was an error loading in-day event ${params.eventId}! Please try again!`, + }, +); diff --git a/src/webui/app/routes/app/workspace/calendar/schedule/stream/$id.tsx b/src/webui/app/routes/app/workspace/calendar/schedule/stream/$id.tsx index 3b9ff3e42..3388efcfc 100644 --- a/src/webui/app/routes/app/workspace/calendar/schedule/stream/$id.tsx +++ b/src/webui/app/routes/app/workspace/calendar/schedule/stream/$id.tsx @@ -63,6 +63,18 @@ const UpdateFormSchema = z.discriminatedUnion("intent", [ z.object({ intent: z.literal("remove"), }), + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), ]); export const handle = { @@ -88,6 +100,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { note: response.note, tags: response.tags as Array<Tag>, allTags: allTags.tags as Array<Tag>, + publishEntity: response.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -169,6 +182,36 @@ export async function action({ request, params }: ActionFunctionArgs) { ); } + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect( + `/app/workspace/calendar/schedule/stream/${id}?${url.searchParams}`, + ); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect( + `/app/workspace/calendar/schedule/stream/${id}?${url.searchParams}`, + ); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect( + `/app/workspace/calendar/schedule/stream/${id}?${url.searchParams}`, + ); + } + default: throw new Response("Bad Intent", { status: 500 }); } @@ -211,6 +254,8 @@ export default function ScheduleStreamViewOne() { entityNotEditable={!corePropertyEditable} entityArchived={loaderData.scheduleStream.archived} returnLocation={`/app/workspace/calendar/schedule/stream?${query}`} + publishable + publishEntity={loaderData.publishEntity ?? undefined} > <GlobalError actionResult={actionData} /> <SectionCard From 4e15b8e278e9537604a15187ea9bc2a200edcefe Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Mon, 8 Jun 2026 23:06:15 +0300 Subject: [PATCH 13/21] New material now --- AGENTS.md | 1 + docs/adrs/0010.entity-publish-mechanism.md | 164 +++++++ .../api/docs/dir_load_public.py | 212 +++++++++ .../api/docs/dir_load_public_from_dir.py | 212 +++++++++ .../api/docs/doc_load_public_from_dir.py | 212 +++++++++ .../jupiter_webapi_client/models/__init__.py | 6 + .../models/dir_load_public_args.py | 62 +++ .../models/dir_load_public_from_dir_args.py | 70 +++ .../models/dir_load_result.py | 38 +- .../models/doc_load_public_from_dir_args.py | 78 +++ gen/ts/webapi-client/gen/index.ts | 3 + .../gen/models/DirLoadPublicArgs.ts | 12 + .../gen/models/DirLoadPublicFromDirArgs.ts | 14 + .../webapi-client/gen/models/DirLoadResult.ts | 2 + .../gen/models/DocLoadPublicFromDirArgs.ts | 15 + .../webapi-client/gen/services/DocsService.ts | 87 ++++ itests/webui/entities/docs.test.py | 45 +- .../service/load_for_date_and_period.py | 2 + .../common/sub/publish/sub/entity/root.py | 2 +- .../component/published-doc-dir-panel.tsx | 446 ++++++++++++++++++ .../jupiter/core/docs/sub/dir/service/load.py | 169 +++++++ .../core/docs/sub/dir/service/published.py | 158 +++++++ .../core/docs/sub/dir/use_case/load.py | 134 +----- .../core/docs/sub/dir/use_case/load_public.py | 44 ++ .../sub/dir/use_case/load_public_from_dir.py | 50 ++ .../sub/doc/use_case/load_public_from_dir.py | 49 ++ .../app/public/published/$externalId.tsx | 4 +- .../doc/dir/$externalId.dir/$dirId.tsx | 93 ++++ .../public/published/doc/dir/$externalId.tsx | 40 ++ .../doc/dirtree/$externalId/$dirId.tsx | 94 ++++ .../doc/dirtree/$externalId/$dirId/$docId.tsx | 94 ++++ .../published/doc/{ => doc}/$externalId.tsx | 0 .../app/routes/app/workspace/docs/$dirId.tsx | 65 ++- 33 files changed, 2547 insertions(+), 130 deletions(-) create mode 100644 docs/adrs/0010.entity-publish-mechanism.md create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/docs/dir_load_public.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/docs/dir_load_public_from_dir.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/api/docs/doc_load_public_from_dir.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/dir_load_public_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/dir_load_public_from_dir_args.py create mode 100644 gen/py/webapi-client/jupiter_webapi_client/models/doc_load_public_from_dir_args.py create mode 100644 gen/ts/webapi-client/gen/models/DirLoadPublicArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/DirLoadPublicFromDirArgs.ts create mode 100644 gen/ts/webapi-client/gen/models/DocLoadPublicFromDirArgs.ts create mode 100644 src/core/jupiter/core/docs/component/published-doc-dir-panel.tsx create mode 100644 src/core/jupiter/core/docs/sub/dir/service/load.py create mode 100644 src/core/jupiter/core/docs/sub/dir/service/published.py create mode 100644 src/core/jupiter/core/docs/sub/dir/use_case/load_public.py create mode 100644 src/core/jupiter/core/docs/sub/dir/use_case/load_public_from_dir.py create mode 100644 src/core/jupiter/core/docs/sub/doc/use_case/load_public_from_dir.py create mode 100644 src/webui/app/routes/app/public/published/doc/dir/$externalId.dir/$dirId.tsx create mode 100644 src/webui/app/routes/app/public/published/doc/dir/$externalId.tsx create mode 100644 src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId.tsx create mode 100644 src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId/$docId.tsx rename src/webui/app/routes/app/public/published/doc/{ => doc}/$externalId.tsx (100%) diff --git a/AGENTS.md b/AGENTS.md index 962e924cc..5287ec7d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,7 @@ Current topics include (non-exhaustive; browse the directory): | 0007 | Search entity indexing and backfill | | 0008 | WebAPI backend blend via `Config.global` | | 0009 | Search index method version | +| 0010 | Entity publish mechanism (public sharing) | ## Generating code diff --git a/docs/adrs/0010.entity-publish-mechanism.md b/docs/adrs/0010.entity-publish-mechanism.md new file mode 100644 index 000000000..dacb4061e --- /dev/null +++ b/docs/adrs/0010.entity-publish-mechanism.md @@ -0,0 +1,164 @@ +# ADR 0010: Entity publish mechanism + +## Status + +Accepted (2026-06-07) + +## Context + +Users need to share read-only snapshots of workspace entities with people who do +not have accounts. The product must: + +- Keep sharing **opt-in** and **revocable** (draft vs active). +- Avoid exposing the full workspace or arbitrary entity IDs to guests. +- Reuse the same domain load logic for workspace and public views where + possible. +- Support both **single entities** (a doc, a person, a habit) and **composite + trees** (a docs folder and its contents, a schedule stream and its events). + +## Decision + +### Core model: `PublishEntity` + +Each publishable workspace entity may have at most one `PublishEntity`, stored +under the workspace’s `PublishDomain`. + +| Field | Role | +| ----- | ---- | +| `owner` | `EntityLink` (`purpose=std`) to the workspace entity being shared | +| `external_id` | UUID v4 used in public URLs (capability-style, unlisted link) | +| `status` | `draft` or `active` — only **active** entities are visible to guests | + +Creation and lifecycle mutations are logged-in use cases: + +- `PublishEntityCreateUseCase` — creates a **draft** publish entity for an owner +- `PublishEntityActivateUseCase` / `PublishEntityToDraftUseCase` — toggle visibility + +Allowed owner types are enumerated in +`ALLOWED_PUBLISH_OWNER_TYPES` in +`src/core/jupiter/core/common/sub/publish/sub/entity/root.py`. New types must be +added there explicitly; creation rejects unknown owner links. + +### Public URL entry point + +The canonical share link shown in the UI is: + +```text +{webUiUrl}/app/public/published/{external_id} +``` + +The route `src/webui/app/routes/app/public/published/$externalId.tsx` loads the +publish entity as a guest, inspects `owner.the_type`, and **redirects** to a +typed public route (for example `/app/public/published/person/{externalId}` or +`/app/public/published/doc/dirtree/{externalId}/{dirRefId}` for directories). + +This keeps one stable link for copy/share while allowing entity-specific public +layouts underneath. + +### Guest access vs workspace access + +| Concern | Workspace (logged in) | Public (guest) | +| ------- | --------------------- | -------------- | +| API client | `getLoggedInApiClient` | `getGuestApiClient` | +| Use case base | `JupiterLoggedIn*` | `JupiterGuestReadonlyUseCase` | +| Publish entity status | Draft and active visible to owner | **Active only** | +| Mutations | Create / activate / draft via publish actions | None (read-only UI) | + +Every `*LoadPublic*` use case follows the same validation skeleton: + +1. Load `PublishEntity` by `external_id`. +2. Reject if status is not `active`. +3. Assert `owner.the_type` matches the expected entity tag. +4. Resolve workspace scope via `PublishDomain` → parent domain (for example + `DocCollection`, `ScheduleDomain`). +5. Assert the target entity belongs to that workspace. +6. Delegate to the entity’s **load service** with `include_publish_entity=False`. + +Shared validation logic for composite trees lives in dedicated services where +needed (for example `DirPublishedLoadService` for docs directories). + +Public loader errors are normalized in +`src/webui/app/rendering/published-loader.server.ts` so guests see 404 for +missing, inactive, or invalid publish links. + +### Workspace UI: managing publish state + +The shared UI component is `PublishPanel` +(`src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx`). It +renders create / activate / draft actions and the public URL field. Route +actions handle form intents `create-publish`, `activate-publish`, and +`to-draft-publish`, calling the publish WebAPI mutations and redirecting back. + +**Where the panel appears depends on layout type:** + +| Layout | Pattern | Examples | +| ------ | ------- | -------- | +| **Leaf** | `LeafPanel` with `publishable` and toolbar publish toggle | Doc, person, habit, chore, … | +| **Branch** | `BranchPanel` with `publishable` | Smart list trunk | +| **Trunk / inline** | Embed `PublishPanel` directly inside route content (own `<Form>`) | Docs folder listing (`$dirId.tsx`) | +| **Publish admin** | Trunk list at Core → Publish; leaf detail embeds `PublishPanel` for an existing `PublishEntity` | `core/publish.tsx`, `$publishEntityId.tsx` | + +`TrunkPanel` intentionally does **not** implement publish chrome. Trunk-level +entities that support sharing embed `PublishPanel` in page content instead. + +Entity load results used by workspace routes may include an optional +`publish_entity` field (loaded via `PublishEntityRepository.load_optional_for_owner` +inside entity load services when `include_publish_entity=True`). + +### Public UI routes + +Each allowed owner type has one or more Remix routes under +`src/webui/app/routes/app/public/published/`. Public views: + +- Use read-only editors or listing components (often the same core components + as workspace, with `inputsEnabled={false}`). +- Use `LeafPanel` for nested navigation where appropriate, but **without** + publish/history affordances. +- Map validation failures to guest-safe 404 responses via + `handlePublishedLoaderError`. + +The redirect table in `$externalId.tsx` must stay in sync with +`ALLOWED_PUBLISH_OWNER_TYPES` and implemented public routes. + +### Composite and implicit sharing + +Some owner types share **more than the single entity** referenced by +`owner`: + +| Published owner | What guests can access | Mechanism | +| --------------- | ------------------------ | --------- | +| **DIR** | The folder and all descendant folders and docs | `DirPublishedLoadService` walks parent links to ensure targets sit under the published root; public navigation uses `/doc/dirtree/{externalId}/…` | +| **SCHEDULE_STREAM** | Stream plus in-day and full-days events in that stream | Root `ScheduleStreamLoadPublic`; nested `*LoadPublicFromScheduleStream` use cases validate event membership | +| **SMART_LIST** | List plus items | `SmartListItemLoadPublicFromSmartList` | +| **METRIC** | Metric plus entries | `MetricEntryLoadPublicFromMetric` | + +Publishing a parent does **not** automatically create child `PublishEntity` +records. Access is enforced in guest load use cases by checking membership in +the published tree. + +### Docs directory routing note + +Published directory browsing uses a dedicated `dirtree/` URL branch +(`/app/public/published/doc/dirtree/{externalId}/{dirId}`) rather than nesting +public dir routes under a parent that re-runs redirect loaders on every child +navigation. `/doc/dir/{externalId}` exists only as a redirect into `dirtree/`. + +## Consequences + +- Adding a new publishable entity type requires: allowlist entry, workspace load + support for `publish_entity` (if applicable), `*LoadPublic*` use case(s), + public WebUI route(s), redirect entry in `$externalId.tsx`, workspace + `PublishPanel` wiring, client codegen, and ideally a WebUI integration test. +- Guest endpoints must never trust client-supplied workspace or entity IDs + without tying them to an active `PublishEntity` and re-validating workspace + membership. +- Draft publish entities are invisible to guests but visible in the owner’s + workspace UI, allowing link preparation before activation. +- Deactivating (moving to draft) immediately revokes guest access without + deleting the publish entity or changing `external_id`. +- Composite types should centralize subtree validation in a service (as with + `DirPublishedLoadService`) rather than duplicating ancestor checks across use + cases. +- Public URLs are unlisted, not authenticated sessions; treat `external_id` as + a capability token and avoid putting sensitive data in entities meant for + sharing. diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/docs/dir_load_public.py b/gen/py/webapi-client/jupiter_webapi_client/api/docs/dir_load_public.py new file mode 100644 index 000000000..73a7ab6af --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/docs/dir_load_public.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.dir_load_public_args import DirLoadPublicArgs +from ...models.dir_load_result import DirLoadResult +from ...models.error_response import ErrorResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: DirLoadPublicArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/dir-load-public", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> DirLoadResult | ErrorResponse | None: + if response.status_code == 200: + response_200 = DirLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[DirLoadResult | ErrorResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: DirLoadPublicArgs | Unset = UNSET, +) -> Response[DirLoadResult | ErrorResponse]: + """Load a published directory by publish external id. + + Args: + body (DirLoadPublicArgs | Unset): DirLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DirLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: DirLoadPublicArgs | Unset = UNSET, +) -> DirLoadResult | ErrorResponse | None: + """Load a published directory by publish external id. + + Args: + body (DirLoadPublicArgs | Unset): DirLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DirLoadResult | ErrorResponse + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: DirLoadPublicArgs | Unset = UNSET, +) -> Response[DirLoadResult | ErrorResponse]: + """Load a published directory by publish external id. + + Args: + body (DirLoadPublicArgs | Unset): DirLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DirLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: DirLoadPublicArgs | Unset = UNSET, +) -> DirLoadResult | ErrorResponse | None: + """Load a published directory by publish external id. + + Args: + body (DirLoadPublicArgs | Unset): DirLoadPublic args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DirLoadResult | ErrorResponse + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/docs/dir_load_public_from_dir.py b/gen/py/webapi-client/jupiter_webapi_client/api/docs/dir_load_public_from_dir.py new file mode 100644 index 000000000..9b40590ce --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/docs/dir_load_public_from_dir.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.dir_load_public_from_dir_args import DirLoadPublicFromDirArgs +from ...models.dir_load_result import DirLoadResult +from ...models.error_response import ErrorResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: DirLoadPublicFromDirArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/dir-load-public-from-dir", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> DirLoadResult | ErrorResponse | None: + if response.status_code == 200: + response_200 = DirLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[DirLoadResult | ErrorResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: DirLoadPublicFromDirArgs | Unset = UNSET, +) -> Response[DirLoadResult | ErrorResponse]: + """Load a subdirectory through a published directory. + + Args: + body (DirLoadPublicFromDirArgs | Unset): DirLoadPublicFromDir args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DirLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: DirLoadPublicFromDirArgs | Unset = UNSET, +) -> DirLoadResult | ErrorResponse | None: + """Load a subdirectory through a published directory. + + Args: + body (DirLoadPublicFromDirArgs | Unset): DirLoadPublicFromDir args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DirLoadResult | ErrorResponse + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: DirLoadPublicFromDirArgs | Unset = UNSET, +) -> Response[DirLoadResult | ErrorResponse]: + """Load a subdirectory through a published directory. + + Args: + body (DirLoadPublicFromDirArgs | Unset): DirLoadPublicFromDir args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DirLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: DirLoadPublicFromDirArgs | Unset = UNSET, +) -> DirLoadResult | ErrorResponse | None: + """Load a subdirectory through a published directory. + + Args: + body (DirLoadPublicFromDirArgs | Unset): DirLoadPublicFromDir args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DirLoadResult | ErrorResponse + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/api/docs/doc_load_public_from_dir.py b/gen/py/webapi-client/jupiter_webapi_client/api/docs/doc_load_public_from_dir.py new file mode 100644 index 000000000..676165061 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/api/docs/doc_load_public_from_dir.py @@ -0,0 +1,212 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.doc_load_public_from_dir_args import DocLoadPublicFromDirArgs +from ...models.doc_load_result import DocLoadResult +from ...models.error_response import ErrorResponse +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + *, + body: DocLoadPublicFromDirArgs | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/doc-load-public-from-dir", + } + + if not isinstance(body, Unset): + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> DocLoadResult | ErrorResponse | None: + if response.status_code == 200: + response_200 = DocLoadResult.from_dict(response.json()) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 406: + response_406 = ErrorResponse.from_dict(response.json()) + + return response_406 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 410: + response_410 = ErrorResponse.from_dict(response.json()) + + return response_410 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 426: + response_426 = ErrorResponse.from_dict(response.json()) + + return response_426 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 502: + response_502 = ErrorResponse.from_dict(response.json()) + + return response_502 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[DocLoadResult | ErrorResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: DocLoadPublicFromDirArgs | Unset = UNSET, +) -> Response[DocLoadResult | ErrorResponse]: + """Load a doc through a published directory. + + Args: + body (DocLoadPublicFromDirArgs | Unset): DocLoadPublicFromDir args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DocLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient | Client, + body: DocLoadPublicFromDirArgs | Unset = UNSET, +) -> DocLoadResult | ErrorResponse | None: + """Load a doc through a published directory. + + Args: + body (DocLoadPublicFromDirArgs | Unset): DocLoadPublicFromDir args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DocLoadResult | ErrorResponse + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: DocLoadPublicFromDirArgs | Unset = UNSET, +) -> Response[DocLoadResult | ErrorResponse]: + """Load a doc through a published directory. + + Args: + body (DocLoadPublicFromDirArgs | Unset): DocLoadPublicFromDir args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[DocLoadResult | ErrorResponse] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: DocLoadPublicFromDirArgs | Unset = UNSET, +) -> DocLoadResult | ErrorResponse | None: + """Load a doc through a published directory. + + Args: + body (DocLoadPublicFromDirArgs | Unset): DocLoadPublicFromDir args. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + DocLoadResult | ErrorResponse + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py index 3b3bfce49..02bf6602f 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/__init__.py @@ -204,6 +204,8 @@ from .dir_find_result import DirFindResult from .dir_find_result_entry import DirFindResultEntry from .dir_load_args import DirLoadArgs +from .dir_load_public_args import DirLoadPublicArgs +from .dir_load_public_from_dir_args import DirLoadPublicFromDirArgs from .dir_load_result import DirLoadResult from .dir_load_result_entry import DirLoadResultEntry from .dir_load_subdir_entry import DirLoadSubdirEntry @@ -224,6 +226,7 @@ from .doc_find_result_entry import DocFindResultEntry from .doc_load_args import DocLoadArgs from .doc_load_public_args import DocLoadPublicArgs +from .doc_load_public_from_dir_args import DocLoadPublicFromDirArgs from .doc_load_result import DocLoadResult from .doc_remove_args import DocRemoveArgs from .doc_update_args import DocUpdateArgs @@ -1241,6 +1244,8 @@ "DirFindResult", "DirFindResultEntry", "DirLoadArgs", + "DirLoadPublicArgs", + "DirLoadPublicFromDirArgs", "DirLoadResult", "DirLoadResultEntry", "DirLoadSubdirEntry", @@ -1261,6 +1266,7 @@ "DocFindResultEntry", "DocLoadArgs", "DocLoadPublicArgs", + "DocLoadPublicFromDirArgs", "DocLoadResult", "DocRemoveArgs", "DocsHelpSubject", diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/dir_load_public_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/dir_load_public_args.py new file mode 100644 index 000000000..07b8831f2 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/dir_load_public_args.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="DirLoadPublicArgs") + + +@_attrs_define +class DirLoadPublicArgs: + """DirLoadPublic args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + """ + + external_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + dir_load_public_args = cls( + external_id=external_id, + ) + + dir_load_public_args.additional_properties = d + return dir_load_public_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/dir_load_public_from_dir_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/dir_load_public_from_dir_args.py new file mode 100644 index 000000000..f8e024b52 --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/dir_load_public_from_dir_args.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="DirLoadPublicFromDirArgs") + + +@_attrs_define +class DirLoadPublicFromDirArgs: + """DirLoadPublicFromDir args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + ref_id (str): A generic entity id. + """ + + external_id: str + ref_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + ref_id = self.ref_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + "ref_id": ref_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + ref_id = d.pop("ref_id") + + dir_load_public_from_dir_args = cls( + external_id=external_id, + ref_id=ref_id, + ) + + dir_load_public_from_dir_args.additional_properties = d + return dir_load_public_from_dir_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/dir_load_result.py b/gen/py/webapi-client/jupiter_webapi_client/models/dir_load_result.py index 9e8e88f7c..1c6d76e63 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/dir_load_result.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/dir_load_result.py @@ -1,15 +1,18 @@ from __future__ import annotations from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field +from ..types import UNSET, Unset + if TYPE_CHECKING: from ..models.dir_ import Dir from ..models.dir_load_result_entry import DirLoadResultEntry from ..models.dir_load_subdir_entry import DirLoadSubdirEntry + from ..models.publish_entity import PublishEntity T = TypeVar("T", bound="DirLoadResult") @@ -23,14 +26,18 @@ class DirLoadResult: dir_ (Dir): A directory in the doc collection. entries (list[DirLoadResultEntry]): subdirs (list[DirLoadSubdirEntry]): + publish_entity (None | PublishEntity | Unset): """ dir_: Dir entries: list[DirLoadResultEntry] subdirs: list[DirLoadSubdirEntry] + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.publish_entity import PublishEntity + dir_ = self.dir_.to_dict() entries = [] @@ -43,6 +50,14 @@ def to_dict(self) -> dict[str, Any]: subdirs_item = subdirs_item_data.to_dict() subdirs.append(subdirs_item) + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -52,6 +67,8 @@ def to_dict(self) -> dict[str, Any]: "subdirs": subdirs, } ) + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @@ -60,6 +77,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.dir_ import Dir from ..models.dir_load_result_entry import DirLoadResultEntry from ..models.dir_load_subdir_entry import DirLoadSubdirEntry + from ..models.publish_entity import PublishEntity d = dict(src_dict) dir_ = Dir.from_dict(d.pop("dir")) @@ -78,10 +96,28 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: subdirs.append(subdirs_item) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + dir_load_result = cls( dir_=dir_, entries=entries, subdirs=subdirs, + publish_entity=publish_entity, ) dir_load_result.additional_properties = d diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/doc_load_public_from_dir_args.py b/gen/py/webapi-client/jupiter_webapi_client/models/doc_load_public_from_dir_args.py new file mode 100644 index 000000000..ab7c781df --- /dev/null +++ b/gen/py/webapi-client/jupiter_webapi_client/models/doc_load_public_from_dir_args.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="DocLoadPublicFromDirArgs") + + +@_attrs_define +class DocLoadPublicFromDirArgs: + """DocLoadPublicFromDir args. + + Attributes: + external_id (str): A GUID external id for a publish entity. + dir_ref_id (str): A generic entity id. + ref_id (str): A generic entity id. + """ + + external_id: str + dir_ref_id: str + ref_id: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + external_id = self.external_id + + dir_ref_id = self.dir_ref_id + + ref_id = self.ref_id + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "external_id": external_id, + "dir_ref_id": dir_ref_id, + "ref_id": ref_id, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + external_id = d.pop("external_id") + + dir_ref_id = d.pop("dir_ref_id") + + ref_id = d.pop("ref_id") + + doc_load_public_from_dir_args = cls( + external_id=external_id, + dir_ref_id=dir_ref_id, + ref_id=ref_id, + ) + + doc_load_public_from_dir_args.additional_properties = d + return doc_load_public_from_dir_args + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/gen/ts/webapi-client/gen/index.ts b/gen/ts/webapi-client/gen/index.ts index f17b2db1e..cd7620c3c 100644 --- a/gen/ts/webapi-client/gen/index.ts +++ b/gen/ts/webapi-client/gen/index.ts @@ -172,6 +172,8 @@ export type { DirFindArgs } from './models/DirFindArgs'; export type { DirFindResult } from './models/DirFindResult'; export type { DirFindResultEntry } from './models/DirFindResultEntry'; export type { DirLoadArgs } from './models/DirLoadArgs'; +export type { DirLoadPublicArgs } from './models/DirLoadPublicArgs'; +export type { DirLoadPublicFromDirArgs } from './models/DirLoadPublicFromDirArgs'; export type { DirLoadResult } from './models/DirLoadResult'; export type { DirLoadResultEntry } from './models/DirLoadResultEntry'; export type { DirLoadSubdirEntry } from './models/DirLoadSubdirEntry'; @@ -191,6 +193,7 @@ export type { DocFindResultEntry } from './models/DocFindResultEntry'; export type { DocIdempotencyKey } from './models/DocIdempotencyKey'; export type { DocLoadArgs } from './models/DocLoadArgs'; export type { DocLoadPublicArgs } from './models/DocLoadPublicArgs'; +export type { DocLoadPublicFromDirArgs } from './models/DocLoadPublicFromDirArgs'; export type { DocLoadResult } from './models/DocLoadResult'; export type { DocName } from './models/DocName'; export type { DocRemoveArgs } from './models/DocRemoveArgs'; diff --git a/gen/ts/webapi-client/gen/models/DirLoadPublicArgs.ts b/gen/ts/webapi-client/gen/models/DirLoadPublicArgs.ts new file mode 100644 index 000000000..f210aa3e4 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/DirLoadPublicArgs.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { PublishExternalId } from './PublishExternalId'; +/** + * DirLoadPublic args. + */ +export type DirLoadPublicArgs = { + external_id: PublishExternalId; +}; + diff --git a/gen/ts/webapi-client/gen/models/DirLoadPublicFromDirArgs.ts b/gen/ts/webapi-client/gen/models/DirLoadPublicFromDirArgs.ts new file mode 100644 index 000000000..20e49b02c --- /dev/null +++ b/gen/ts/webapi-client/gen/models/DirLoadPublicFromDirArgs.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { PublishExternalId } from './PublishExternalId'; +/** + * DirLoadPublicFromDir args. + */ +export type DirLoadPublicFromDirArgs = { + external_id: PublishExternalId; + ref_id: EntityId; +}; + diff --git a/gen/ts/webapi-client/gen/models/DirLoadResult.ts b/gen/ts/webapi-client/gen/models/DirLoadResult.ts index b5e2ae07b..bd6703405 100644 --- a/gen/ts/webapi-client/gen/models/DirLoadResult.ts +++ b/gen/ts/webapi-client/gen/models/DirLoadResult.ts @@ -5,6 +5,7 @@ import type { Dir } from './Dir'; import type { DirLoadResultEntry } from './DirLoadResultEntry'; import type { DirLoadSubdirEntry } from './DirLoadSubdirEntry'; +import type { PublishEntity } from './PublishEntity'; /** * Loaded directory, its docs, and immediate child directories. */ @@ -12,5 +13,6 @@ export type DirLoadResult = { dir: Dir; entries: Array<DirLoadResultEntry>; subdirs: Array<DirLoadSubdirEntry>; + publish_entity?: (PublishEntity | null); }; diff --git a/gen/ts/webapi-client/gen/models/DocLoadPublicFromDirArgs.ts b/gen/ts/webapi-client/gen/models/DocLoadPublicFromDirArgs.ts new file mode 100644 index 000000000..798285744 --- /dev/null +++ b/gen/ts/webapi-client/gen/models/DocLoadPublicFromDirArgs.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { EntityId } from './EntityId'; +import type { PublishExternalId } from './PublishExternalId'; +/** + * DocLoadPublicFromDir args. + */ +export type DocLoadPublicFromDirArgs = { + external_id: PublishExternalId; + dir_ref_id: EntityId; + ref_id: EntityId; +}; + diff --git a/gen/ts/webapi-client/gen/services/DocsService.ts b/gen/ts/webapi-client/gen/services/DocsService.ts index 69c0add9d..39f1c257d 100644 --- a/gen/ts/webapi-client/gen/services/DocsService.ts +++ b/gen/ts/webapi-client/gen/services/DocsService.ts @@ -8,6 +8,8 @@ import type { DirCreateResult } from '../models/DirCreateResult'; import type { DirFindArgs } from '../models/DirFindArgs'; import type { DirFindResult } from '../models/DirFindResult'; import type { DirLoadArgs } from '../models/DirLoadArgs'; +import type { DirLoadPublicArgs } from '../models/DirLoadPublicArgs'; +import type { DirLoadPublicFromDirArgs } from '../models/DirLoadPublicFromDirArgs'; import type { DirLoadResult } from '../models/DirLoadResult'; import type { DirRemoveArgs } from '../models/DirRemoveArgs'; import type { DirUpdateArgs } from '../models/DirUpdateArgs'; @@ -18,6 +20,7 @@ import type { DocFindArgs } from '../models/DocFindArgs'; import type { DocFindResult } from '../models/DocFindResult'; import type { DocLoadArgs } from '../models/DocLoadArgs'; import type { DocLoadPublicArgs } from '../models/DocLoadPublicArgs'; +import type { DocLoadPublicFromDirArgs } from '../models/DocLoadPublicFromDirArgs'; import type { DocLoadResult } from '../models/DocLoadResult'; import type { DocRemoveArgs } from '../models/DocRemoveArgs'; import type { DocUpdateArgs } from '../models/DocUpdateArgs'; @@ -137,6 +140,62 @@ export class DocsService { }, }); } + /** + * Load a published directory by publish external id. + * @param requestBody The input data + * @returns DirLoadResult Successful response + * @throws ApiError + */ + public dirLoadPublic( + requestBody?: DirLoadPublicArgs, + ): CancelablePromise<DirLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/dir-load-public', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } + /** + * Load a subdirectory through a published directory. + * @param requestBody The input data + * @returns DirLoadResult Successful response + * @throws ApiError + */ + public dirLoadPublicFromDir( + requestBody?: DirLoadPublicFromDirArgs, + ): CancelablePromise<DirLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/dir-load-public-from-dir', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * Use case for removing a directory. * @param requestBody The input data @@ -333,6 +392,34 @@ export class DocsService { }, }); } + /** + * Load a doc through a published directory. + * @param requestBody The input data + * @returns DocLoadResult Successful response + * @throws ApiError + */ + public docLoadPublicFromDir( + requestBody?: DocLoadPublicFromDirArgs, + ): CancelablePromise<DocLoadResult> { + return this.httpRequest.request({ + method: 'POST', + url: '/doc-load-public-from-dir', + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error response for EntityAlreadyExistsError`, + 401: `Error response for ExpiredAuthTokenError`, + 404: `Error response for EntityNotFoundError`, + 406: `Error response for UnavailableGloballyError, UnavailableForComponentError, UnavailableForContextError`, + 409: `Error response for UserAlreadyExistsButIsArchivedError, TimePlanExistsForDatePeriodCombinationError, BigPlanMilestoneAlreadyExistsForDateError, JournalExistsForDatePeriodCombinationError, ContactAlreadyExistsError, TagAlreadyExistsError, EntityIsAlreadyActiveError, EntityIsAlreadyDraftError`, + 410: `Error response for UserNotFoundError, WorkspaceNotFoundError`, + 422: `Error response for JSONDecodeError, InputValidationError, MultiInputValidationError, RealmDecodingError, UserAlreadyExistsError, WorkspaceAlreadyExistsError, InvalidLoginCredentialsError, InvalidLoginMethodError, InvalidAPIKeyError, AspectInSignificantUseError, UserEmailAlreadyVerifiedError, ContactInSignificantUseError, InvalidEmailAttemptVerificationStateError, EmailAttemptVerificationExpiredError, NoActiveEmailVerificationAttemptError`, + 426: `Error response for InvalidAuthTokenError`, + 429: `Error response for TooManyEmailVerificationAttemptsError`, + 502: `Error response for EmailSendError`, + }, + }); + } /** * The command for removing a doc. * @param requestBody The input data diff --git a/itests/webui/entities/docs.test.py b/itests/webui/entities/docs.test.py index a0c1c6250..0114a86db 100644 --- a/itests/webui/entities/docs.test.py +++ b/itests/webui/entities/docs.test.py @@ -369,7 +369,50 @@ def test_webui_doc_publish_and_view_public(page: Page, create_doc) -> None: assert "/app/public/published/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/doc/")) + page.wait_for_url(re.compile(r"/app/public/published/doc/doc/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Published Doc") + + +def test_webui_dir_publish_and_view_public(page: Page, create_dir, create_doc) -> None: + folder = create_dir("Published Folder") + doc = create_doc( + "Nested Published Doc", + "Nested doc body", + parent_dir_ref_id=folder.ref_id, + ) + + page.goto(f"/app/workspace/docs/{folder.ref_id}") + page.wait_for_selector("#trunk-panel") + page.wait_for_selector("#Dir-publish") + + page.locator("button[id='Dir-publish-create']").click() + page.wait_for_url(re.compile(rf"/app/workspace/docs/{folder.ref_id}")) + page.wait_for_selector("#Dir-publish") + expect(page.locator("#Dir-publish")).to_contain_text("draft") + + page.locator("button[id='Dir-publish-toggle-status']").click() + page.wait_for_url(re.compile(rf"/app/workspace/docs/{folder.ref_id}")) + page.wait_for_selector("#Dir-publish") + expect(page.locator("#Dir-publish")).to_contain_text("active") + + public_url = page.locator('input[name="publicUrl"]').input_value() + assert "/app/public/published/" in public_url + + page.goto(public_url) + page.wait_for_url( + re.compile(rf"/app/public/published/doc/dirtree/[^/]+/{folder.ref_id}$") + ) + page.wait_for_selector("#leaf-panel") + + expect(page.locator(f"#doc-{doc.ref_id}")).to_contain_text("Nested Published Doc") + + page.locator(f"#doc-{doc.ref_id} a").click() + page.wait_for_url( + re.compile( + rf"/app/public/published/doc/dirtree/[^/]+/{folder.ref_id}/{doc.ref_id}" + ) + ) + page.wait_for_selector("#leaf-panel") + expect(page.locator('input[name="name"]')).to_have_value("Nested Published Doc") diff --git a/src/core/jupiter/core/calendar/service/load_for_date_and_period.py b/src/core/jupiter/core/calendar/service/load_for_date_and_period.py index b28196d63..ba2004b5c 100644 --- a/src/core/jupiter/core/calendar/service/load_for_date_and_period.py +++ b/src/core/jupiter/core/calendar/service/load_for_date_and_period.py @@ -197,6 +197,7 @@ async def build_entries( schedule_streams_by_ref_id: dict[EntityId, ScheduleStream], schedule_stream_ref_id: EntityId | None = None, ) -> CalendarEventsEntries: + """Build calendar entries for the schedule period.""" time_events_full_days: list[TimeEventFullDaysBlock] = await uow.get( TimeEventFullDaysBlockRepository ).find_all_between( @@ -629,6 +630,7 @@ async def build_stats( schedule_domain: ScheduleDomain | None = None, schedule_stream_ref_id: EntityId | None = None, ) -> CalendarEventsStats: + """Build calendar stats for the schedule period.""" if schedule_stream_ref_id is not None: if schedule_domain is None: raise InputValidationError( diff --git a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py index 0b52001da..74f1b0762 100644 --- a/src/core/jupiter/core/common/sub/publish/sub/entity/root.py +++ b/src/core/jupiter/core/common/sub/publish/sub/entity/root.py @@ -35,7 +35,7 @@ NamedEntityTag.CHORE.value, # done NamedEntityTag.BIG_PLAN.value, # done NamedEntityTag.DOC.value, # done - NamedEntityTag.DIR.value, + NamedEntityTag.DIR.value, # done NamedEntityTag.JOURNAL.value, # done NamedEntityTag.VACATION.value, # done NamedEntityTag.SMART_LIST.value, # done diff --git a/src/core/jupiter/core/docs/component/published-doc-dir-panel.tsx b/src/core/jupiter/core/docs/component/published-doc-dir-panel.tsx new file mode 100644 index 000000000..4a1f8a248 --- /dev/null +++ b/src/core/jupiter/core/docs/component/published-doc-dir-panel.tsx @@ -0,0 +1,446 @@ +import { + DocsHelpSubject, + type DirLoadResult, + type DirLoadResultEntry, + type DirLoadSubdirEntry, + type Tag, +} from "@jupiter/webapi-client"; +import { Box, Typography } from "@mui/material"; +import { styled } from "@mui/material/styles"; +import { Outlet } from "@remix-run/react"; +import { AnimatePresence } from "framer-motion"; +import { useContext, useMemo, useState } from "react"; + +import { EntityNameOneLineComponent } from "#/core/common/component/entity-name"; +import { TimeDiffTag } from "#/core/common/component/time-diff-tag"; +import { + DEFAULT_NOTE_CONTENT_PREVIEW_MAX_CHARS, + noteContentPreviewPlainText, +} from "#/core/common/sub/notes/note-content-plain-text"; +import { TagTag } from "#/core/common/sub/tags/component/tag-tag"; +import { isDirRoot } from "#/core/docs/sub/dir/root"; +import { EntityCard, EntityLink } from "#/core/infra/component/entity-card"; +import { EntityNoNothingCard } from "#/core/infra/component/entity-no-nothing-card"; +import { EntityStack } from "#/core/infra/component/entity-stack"; +import { LeafPanel } from "#/core/infra/component/layout/leaf-panel"; +import { NestingAwareBlock } from "#/core/infra/component/layout/nesting-aware-block"; +import { SectionCard } from "#/core/infra/component/section-card"; +import { + FilterFewOptionsCompact, + FilterManyOptions, + SectionActions, +} from "#/core/infra/component/section-actions"; +import { useBigScreen } from "#/core/infra/component/use-big-screen"; +import { useLeafNeedsToShowLeaflet } from "#/core/infra/component/use-nested-entities"; +import { LeafPanelExpansionState } from "#/core/infra/leaf-panel-expansion"; +import { TopLevelInfoContext } from "#/core/infra/top-level-context"; + +enum DocsSortOrder { + CREATED_ASC = "created_asc", + CREATED_DESC = "created_desc", + MODIFIED_ASC = "modified_asc", + MODIFIED_DESC = "modified_desc", + NAME_ASC = "name_asc", + NAME_DESC = "name_desc", +} + +interface PublishedDocDirPanelProps { + externalId: string; + publishedRootDirRefId: string; + dirLoad: DirLoadResult; + allTags: Tag[]; + returnLocation: string; +} + +export function PublishedDocDirPanel(props: PublishedDocDirPanelProps) { + const shouldShowALeaflet = useLeafNeedsToShowLeaflet(); + const topLevelInfo = useContext(TopLevelInfoContext); + const isBigScreen = useBigScreen(); + const compactDocCardLayout = !isBigScreen; + + const { dirLoad, externalId, publishedRootDirRefId } = props; + const dirId = dirLoad.dir.ref_id; + const basePath = `/app/public/published/doc/dirtree/${externalId}`; + + const [selectedTagsRefId, setSelectedTagsRefId] = useState<string[]>([]); + const [sortOrder, setSortOrder] = useState<DocsSortOrder>( + DocsSortOrder.MODIFIED_DESC, + ); + + const filteredSubdirs = + selectedTagsRefId.length === 0 + ? dirLoad.subdirs + : dirLoad.subdirs.filter((entry) => + entry.tags.some((tag: Tag) => selectedTagsRefId.includes(tag.ref_id)), + ); + + const filteredEntries = + selectedTagsRefId.length === 0 + ? dirLoad.entries + : dirLoad.entries.filter((entry) => + entry.tags.some((tag: Tag) => selectedTagsRefId.includes(tag.ref_id)), + ); + + const sortedFilteredSubdirs = useMemo( + () => sortSubdirEntries(filteredSubdirs, sortOrder), + [filteredSubdirs, sortOrder], + ); + + const sortedFilteredEntries = useMemo( + () => sortDocEntries(filteredEntries, sortOrder), + [filteredEntries, sortOrder], + ); + + const showParentLink = + !isDirRoot(dirLoad.dir) && dirId !== publishedRootDirRefId; + const parentHref = + dirLoad.dir.parent_dir_ref_id === publishedRootDirRefId + ? `${basePath}/${publishedRootDirRefId}` + : `${basePath}/${dirLoad.dir.parent_dir_ref_id}`; + + const listIsEmpty = + sortedFilteredSubdirs.length === 0 && sortedFilteredEntries.length === 0; + + return ( + <LeafPanel + key={`published-docs-dir-${dirId}`} + fakeKey={`published-docs-dir-${dirId}`} + inputsEnabled={false} + entityNotEditable={true} + disabled={true} + returnLocation={props.returnLocation} + initialExpansionState={LeafPanelExpansionState.FULL} + allowedExpansionStates={[LeafPanelExpansionState.FULL]} + shouldShowALeaflet={shouldShowALeaflet} + > + <NestingAwareBlock shouldHide={shouldShowALeaflet}> + <SectionCard + title={dirLoad.dir.name} + actions={ + <SectionActions + id="published-docs-actions" + topLevelInfo={topLevelInfo} + inputsEnabled={false} + actions={[ + FilterFewOptionsCompact( + "Sort", + sortOrder, + [ + { + value: DocsSortOrder.CREATED_ASC, + text: "Creation date (oldest first)", + }, + { + value: DocsSortOrder.CREATED_DESC, + text: "Creation date (newest first)", + }, + { + value: DocsSortOrder.MODIFIED_ASC, + text: "Last modified (oldest first)", + }, + { + value: DocsSortOrder.MODIFIED_DESC, + text: "Last modified (newest first)", + }, + { + value: DocsSortOrder.NAME_ASC, + text: "Name (A–Z)", + }, + { + value: DocsSortOrder.NAME_DESC, + text: "Name (Z–A)", + }, + ], + setSortOrder, + ), + FilterManyOptions( + "Tags", + props.allTags.map((tag) => ({ + value: tag.ref_id, + text: tag.name, + })), + setSelectedTagsRefId, + ), + ]} + /> + } + > + {listIsEmpty && !showParentLink && ( + <EntityNoNothingCard + title="Empty Folder" + message="This published folder has no subfolders or docs to show." + helpSubject={DocsHelpSubject.DOCS} + /> + )} + + <EntityStack> + {showParentLink && ( + <EntityCard key="docs-parent" entityId="docs-parent"> + <EntityLink to={parentHref} singleLine> + <DocCardRoot> + <DocCardTitleRow> + <DocCardTitleSlot> + <Typography variant="body1" component="span"> + .. + </Typography> + </DocCardTitleSlot> + </DocCardTitleRow> + </DocCardRoot> + </EntityLink> + </EntityCard> + )} + {sortedFilteredSubdirs.map((entry) => ( + <EntityCard + key={`dir-${entry.dir.ref_id}`} + entityId={`dir-${entry.dir.ref_id}`} + > + <EntityLink to={`${basePath}/${entry.dir.ref_id}`} singleLine> + <DocCardRoot> + <DocCardTitleRow> + <DocCardTitleSlot> + <DocCardTitleTextWrap> + <DirTitleWithFolderGlyph> + <DirFolderGlyph aria-hidden>📁</DirFolderGlyph> + <EntityNameOneLineComponent name={entry.dir.name} /> + </DirTitleWithFolderGlyph> + </DocCardTitleTextWrap> + </DocCardTitleSlot> + {!compactDocCardLayout && + entry.tags.map((tag: Tag) => ( + <DocCardTitleAffix key={tag.ref_id}> + <TagTag tag={tag} /> + </DocCardTitleAffix> + ))} + <DocCardTitleAffix> + <TimeDiffTag + today={topLevelInfo.today} + labelPrefix="Last modified" + collectionTime={entry.dir.last_modified_time} + compact={compactDocCardLayout} + /> + </DocCardTitleAffix> + </DocCardTitleRow> + {compactDocCardLayout && entry.tags.length > 0 && ( + <DocCardTagsRow> + {entry.tags.map((tag: Tag) => ( + <TagTag key={tag.ref_id} tag={tag} /> + ))} + </DocCardTagsRow> + )} + </DocCardRoot> + </EntityLink> + </EntityCard> + ))} + {sortedFilteredEntries.map((entry) => ( + <EntityCard + key={`doc-${entry.doc.ref_id}`} + entityId={`doc-${entry.doc.ref_id}`} + > + <EntityLink + to={`${basePath}/${dirId}/${entry.doc.ref_id}`} + singleLine + > + <DocCardRoot> + <DocCardTitleRow> + <DocCardTitleSlot> + <DocCardTitleTextWrap> + <EntityNameOneLineComponent name={entry.doc.name} /> + </DocCardTitleTextWrap> + </DocCardTitleSlot> + {!compactDocCardLayout && + entry.tags.map((tag: Tag) => ( + <DocCardTitleAffix key={tag.ref_id}> + <TagTag tag={tag} /> + </DocCardTitleAffix> + ))} + <DocCardTitleAffix> + <TimeDiffTag + today={topLevelInfo.today} + labelPrefix="Last modified" + collectionTime={entry.doc.last_modified_time} + compact={compactDocCardLayout} + /> + </DocCardTitleAffix> + </DocCardTitleRow> + {compactDocCardLayout && entry.tags.length > 0 && ( + <DocCardTagsRow> + {entry.tags.map((tag: Tag) => ( + <TagTag key={tag.ref_id} tag={tag} /> + ))} + </DocCardTagsRow> + )} + {noteContentPreviewPlainText( + entry.note, + DEFAULT_NOTE_CONTENT_PREVIEW_MAX_CHARS, + ) && ( + <DocCardPreview variant="body2"> + {noteContentPreviewPlainText( + entry.note, + DEFAULT_NOTE_CONTENT_PREVIEW_MAX_CHARS, + )} + </DocCardPreview> + )} + </DocCardRoot> + </EntityLink> + </EntityCard> + ))} + </EntityStack> + </SectionCard> + </NestingAwareBlock> + <AnimatePresence mode="wait" initial={false}> + <Outlet /> + </AnimatePresence> + </LeafPanel> + ); +} + +const DocCardRoot = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + alignItems: "stretch", + flexGrow: 1, + flexBasis: 0, + minWidth: 0, + maxWidth: "100%", + width: "100%", + gap: theme.spacing(0.75), +})); + +const DocCardTitleRow = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "row", + flexWrap: "nowrap", + alignItems: "center", + gap: theme.spacing(0.5), + width: "100%", + maxWidth: "100%", + minWidth: 0, + overflow: "hidden", +})); + +const DocCardTitleSlot = styled(Box)({ + flex: "1 1 0%", + flexBasis: 0, + minWidth: 0, + maxWidth: "100%", + overflow: "hidden", +}); + +const DocCardTitleTextWrap = styled(Box)({ + display: "block", + minWidth: 0, + maxWidth: "100%", + overflow: "hidden", + "& .MuiTypography-root": { + width: "100%", + }, +}); + +const DirTitleWithFolderGlyph = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "row", + alignItems: "center", + gap: theme.spacing(0.5), + minWidth: 0, + maxWidth: "100%", + overflow: "hidden", + "& .MuiTypography-root": { + width: "100%", + }, +})); + +const DirFolderGlyph = styled(Box)({ + flexShrink: 0, + lineHeight: 1, +}); + +const DocCardTitleAffix = styled(Box)({ + flexShrink: 0, +}); + +const DocCardTagsRow = styled(Box)(({ theme }) => ({ + display: "flex", + flexWrap: "wrap", + alignItems: "center", + gap: theme.spacing(0.5), + width: "100%", +})); + +const DocCardPreview = styled(Typography)(({ theme }) => ({ + fontSize: "0.8rem", + lineHeight: 1.35, + display: "-webkit-box", + WebkitLineClamp: 2, + WebkitBoxOrient: "vertical", + overflow: "hidden", + width: "100%", + color: theme.palette.text.secondary, +})); + +function sortDocEntries( + entries: DirLoadResultEntry[], + order: DocsSortOrder, +): DirLoadResultEntry[] { + const sorted = [...entries]; + const byCreatedAsc = (a: DirLoadResultEntry, b: DirLoadResultEntry) => + a.doc.created_time.localeCompare(b.doc.created_time); + const byModifiedAsc = (a: DirLoadResultEntry, b: DirLoadResultEntry) => + a.doc.last_modified_time.localeCompare(b.doc.last_modified_time); + const byNameAsc = (a: DirLoadResultEntry, b: DirLoadResultEntry) => + a.doc.name.localeCompare(b.doc.name); + switch (order) { + case DocsSortOrder.CREATED_ASC: + sorted.sort(byCreatedAsc); + break; + case DocsSortOrder.CREATED_DESC: + sorted.sort((a, b) => byCreatedAsc(b, a)); + break; + case DocsSortOrder.MODIFIED_ASC: + sorted.sort(byModifiedAsc); + break; + case DocsSortOrder.MODIFIED_DESC: + sorted.sort((a, b) => byModifiedAsc(b, a)); + break; + case DocsSortOrder.NAME_ASC: + sorted.sort(byNameAsc); + break; + case DocsSortOrder.NAME_DESC: + sorted.sort((a, b) => byNameAsc(b, a)); + break; + } + return sorted; +} + +function sortSubdirEntries( + entries: DirLoadSubdirEntry[], + order: DocsSortOrder, +): DirLoadSubdirEntry[] { + const sorted = [...entries]; + const byCreatedAsc = (a: DirLoadSubdirEntry, b: DirLoadSubdirEntry) => + a.dir.created_time.localeCompare(b.dir.created_time); + const byModifiedAsc = (a: DirLoadSubdirEntry, b: DirLoadSubdirEntry) => + a.dir.last_modified_time.localeCompare(b.dir.last_modified_time); + const byNameAsc = (a: DirLoadSubdirEntry, b: DirLoadSubdirEntry) => + a.dir.name.localeCompare(b.dir.name); + switch (order) { + case DocsSortOrder.CREATED_ASC: + sorted.sort(byCreatedAsc); + break; + case DocsSortOrder.CREATED_DESC: + sorted.sort((a, b) => byCreatedAsc(b, a)); + break; + case DocsSortOrder.MODIFIED_ASC: + sorted.sort(byModifiedAsc); + break; + case DocsSortOrder.MODIFIED_DESC: + sorted.sort((a, b) => byModifiedAsc(b, a)); + break; + case DocsSortOrder.NAME_ASC: + sorted.sort(byNameAsc); + break; + case DocsSortOrder.NAME_DESC: + sorted.sort((a, b) => byNameAsc(b, a)); + break; + } + return sorted; +} diff --git a/src/core/jupiter/core/docs/sub/dir/service/load.py b/src/core/jupiter/core/docs/sub/dir/service/load.py new file mode 100644 index 000000000..c8e8e6a80 --- /dev/null +++ b/src/core/jupiter/core/docs/sub/dir/service/load.py @@ -0,0 +1,169 @@ +"""Shared service for loading a directory listing.""" + +from typing import cast + +from jupiter.core.common.sub.notes.collection import NoteCollection +from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntityRepository +from jupiter.core.common.sub.tags.root import TagDomain +from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.tags.sub.tag.root import Tag +from jupiter.core.docs.sub.dir.root import Dir +from jupiter.core.docs.sub.dir.use_case.load import ( + DirLoadResult, + DirLoadResultEntry, + DirLoadSubdirEntry, +) +from jupiter.core.docs.sub.doc.root import Doc +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.base.entity_link import EntityLink +from jupiter.framework.entity import NoFilter +from jupiter.framework.errors import InputValidationError +from jupiter.framework.storage.repository import DomainUnitOfWork + + +class DirLoadService: + """Shared service for loading a directory and its immediate children.""" + + async def do_it( + self, + uow: DomainUnitOfWork, + workspace_ref_id: EntityId, + doc_collection_ref_id: EntityId, + dir_entity: Dir, + *, + allow_archived: bool = False, + filter_ref_ids: list[EntityId] | None = None, + include_publish_entity: bool = True, + ) -> DirLoadResult: + """Load a directory with docs, child dirs, and optional publish entity.""" + dir_entity = await uow.get_for(Dir).load_by_id( + dir_entity.ref_id, + allow_archived=allow_archived, + ) + if dir_entity.doc_collection.ref_id != doc_collection_ref_id: + raise InputValidationError("Directory is not in this workspace.") + + subdirs_raw = await uow.get_for(Dir).find_all_generic( + parent_ref_id=doc_collection_ref_id, + allow_archived=allow_archived, + parent_dir_ref_id=[dir_entity.ref_id], + ) + subdirs_sorted = sorted(subdirs_raw, key=lambda d: str(d.name)) + + docs = await uow.get_for(Doc).find_all_generic( + parent_ref_id=doc_collection_ref_id, + allow_archived=allow_archived, + ref_id=filter_ref_ids or NoFilter(), + parent_dir_ref_id=[dir_entity.ref_id], + ) + + note_collection = await uow.get_for(NoteCollection).load_by_parent( + workspace_ref_id + ) + notes = await uow.get(NoteRepository).find_all_for_note_collection( + note_collection_ref_id=note_collection.ref_id, + allow_archived=True, + filter_owners=[ + EntityLink.std(NamedEntityTag.DOC.value, rid) + for rid in [d.ref_id for d in docs] + ], + ) + notes_by_doc_ref_id: dict[EntityId, Note] = {} + for n in notes: + notes_by_doc_ref_id[n.owner.ref_id] = n + + tags_domain = await uow.get_for(TagDomain).load_by_parent(workspace_ref_id) + doc_tag_links = await uow.get(TagLinkRepository).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=False, + owner=[EntityLink.std(NamedEntityTag.DOC.value, d.ref_id) for d in docs], + ) + doc_tag_links_by_doc_ref_id = { + cast(EntityId, tl.owner.ref_id): tl for tl in doc_tag_links + } + dir_tag_links = ( + await uow.get(TagLinkRepository).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=False, + owner=[ + EntityLink.std(NamedEntityTag.DIR.value, sd.ref_id) + for sd in subdirs_sorted + ], + ) + if subdirs_sorted + else [] + ) + dir_tag_links_by_dir_ref_id = { + cast(EntityId, tl.owner.ref_id): tl for tl in dir_tag_links + } + all_tag_ref_ids: list[EntityId] = [] + for tl in doc_tag_links: + all_tag_ref_ids.extend(tl.ref_ids) + for tl in dir_tag_links: + all_tag_ref_ids.extend(tl.ref_ids) + if all_tag_ref_ids: + all_tags = await uow.get_for(Tag).find_all_generic( + parent_ref_id=tags_domain.ref_id, + allow_archived=False, + ref_id=list(set(all_tag_ref_ids)), + ) + all_tags_by_ref_id = {t.ref_id: t for t in all_tags} + else: + all_tags_by_ref_id = {} + + entries: list[DirLoadResultEntry] = [] + for doc in docs: + note = notes_by_doc_ref_id.get(doc.ref_id) + if note is None: + raise InputValidationError( + f"Doc {doc.ref_id} has no associated note.", + ) + entries.append( + DirLoadResultEntry( + doc=doc, + tags=( + [ + all_tags_by_ref_id[rid] + for rid in doc_tag_links_by_doc_ref_id[doc.ref_id].ref_ids + if rid in all_tags_by_ref_id + ] + if doc.ref_id in doc_tag_links_by_doc_ref_id + else [] + ), + note=note, + ) + ) + + subdir_entries = [ + DirLoadSubdirEntry( + dir=sd, + tags=( + [ + all_tags_by_ref_id[rid] + for rid in dir_tag_links_by_dir_ref_id[sd.ref_id].ref_ids + if rid in all_tags_by_ref_id + ] + if sd.ref_id in dir_tag_links_by_dir_ref_id + else [] + ), + ) + for sd in subdirs_sorted + ] + + publish_entity = None + if include_publish_entity: + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std(NamedEntityTag.DIR.value, dir_entity.ref_id), + allow_archived=allow_archived, + ) + + return DirLoadResult( + dir=dir_entity, + entries=entries, + subdirs=subdir_entries, + publish_entity=publish_entity, + ) diff --git a/src/core/jupiter/core/docs/sub/dir/service/published.py b/src/core/jupiter/core/docs/sub/dir/service/published.py new file mode 100644 index 000000000..b1c0b49db --- /dev/null +++ b/src/core/jupiter/core/docs/sub/dir/service/published.py @@ -0,0 +1,158 @@ +"""Shared service for loading published doc directory subtrees.""" + +from dataclasses import dataclass + +from jupiter.core.common.sub.publish.root import PublishDomain +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) +from jupiter.core.common.sub.publish.sub.entity.status import PublishEntityStatus +from jupiter.core.docs.root import DocCollection +from jupiter.core.docs.sub.dir.root import Dir +from jupiter.core.docs.sub.doc.root import Doc +from jupiter.core.named_entity_tag import NamedEntityTag +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.errors import InputValidationError +from jupiter.framework.storage.repository import DomainUnitOfWork + + +@dataclass(frozen=True) +class PublishedDirContext: + """Resolved publish context for a published directory.""" + + publish_entity: PublishEntity + doc_collection: DocCollection + published_dir: Dir + + +class DirPublishedLoadService: + """Shared service for validating and loading published directory subtrees.""" + + async def load_context( + self, + uow: DomainUnitOfWork, + external_id: PublishExternalId, + ) -> PublishedDirContext: + """Load and validate an active publish entity for a directory.""" + publish_entity = await uow.get(PublishEntityRepository).load_by_external_id( + external_id + ) + + if publish_entity.status != PublishEntityStatus.ACTIVE: + raise InputValidationError( + "The publish entity is not active and cannot be loaded." + ) + + if publish_entity.owner.the_type != NamedEntityTag.DIR.value: + raise InputValidationError( + "The publish entity does not refer to a directory." + ) + if publish_entity.owner.purpose != "std": + raise InputValidationError( + "The publish entity owner link purpose must be 'std'." + ) + + publish_domain = await uow.get_for(PublishDomain).load_by_id( + publish_entity.publish_domain.ref_id + ) + doc_collection = await uow.get_for(DocCollection).load_by_parent( + publish_domain.workspace.ref_id + ) + published_dir = await uow.get_for(Dir).load_by_id( + publish_entity.owner.ref_id, + allow_archived=False, + ) + if published_dir.doc_collection.ref_id != doc_collection.ref_id: + raise InputValidationError( + "The publish entity does not refer to a workspace directory." + ) + + return PublishedDirContext( + publish_entity=publish_entity, + doc_collection=doc_collection, + published_dir=published_dir, + ) + + async def load_dir_under_published_root( + self, + uow: DomainUnitOfWork, + context: PublishedDirContext, + target_dir_ref_id: EntityId, + ) -> Dir: + """Load a directory that is the published root or one of its descendants.""" + if not await self._dir_is_under_published_root( + uow, + context.doc_collection.ref_id, + context.published_dir.ref_id, + target_dir_ref_id, + ): + raise InputValidationError( + "The directory is not part of the published directory tree." + ) + + return await uow.get_for(Dir).load_by_id( + target_dir_ref_id, + allow_archived=False, + ) + + async def load_doc_under_published_dir( + self, + uow: DomainUnitOfWork, + context: PublishedDirContext, + parent_dir_ref_id: EntityId, + doc_ref_id: EntityId, + ) -> Doc: + """Load a doc that belongs to a directory under the published root.""" + if not await self._dir_is_under_published_root( + uow, + context.doc_collection.ref_id, + context.published_dir.ref_id, + parent_dir_ref_id, + ): + raise InputValidationError( + "The directory is not part of the published directory tree." + ) + + doc = await uow.get_for(Doc).load_by_id( + doc_ref_id, + allow_archived=False, + ) + if doc.doc_collection.ref_id != context.doc_collection.ref_id: + raise InputValidationError("The doc does not belong to this workspace.") + if doc.parent_dir_ref_id != parent_dir_ref_id: + raise InputValidationError( + "The doc does not belong to the requested directory." + ) + + return doc + + async def _dir_is_under_published_root( + self, + uow: DomainUnitOfWork, + doc_collection_ref_id: EntityId, + published_dir_ref_id: EntityId, + target_dir_ref_id: EntityId, + ) -> bool: + """Return True if target_dir is the published root or a descendant.""" + if target_dir_ref_id == published_dir_ref_id: + return True + + dir_entity = await uow.get_for(Dir).load_by_id( + target_dir_ref_id, + allow_archived=False, + ) + if dir_entity.doc_collection.ref_id != doc_collection_ref_id: + return False + + current = dir_entity + while current.parent_dir_ref_id is not None: + if current.parent_dir_ref_id == published_dir_ref_id: + return True + current = await uow.get_for(Dir).load_by_id( + current.parent_dir_ref_id, + allow_archived=False, + ) + + return False diff --git a/src/core/jupiter/core/docs/sub/dir/use_case/load.py b/src/core/jupiter/core/docs/sub/dir/use_case/load.py index bef95440a..17e1567a5 100644 --- a/src/core/jupiter/core/docs/sub/dir/use_case/load.py +++ b/src/core/jupiter/core/docs/sub/dir/use_case/load.py @@ -1,12 +1,8 @@ """Load a directory with its docs (notes and tags), and immediate child directories.""" -from typing import cast - from jupiter.core.app import AppCore -from jupiter.core.common.sub.notes.collection import NoteCollection -from jupiter.core.common.sub.notes.root import Note, NoteRepository -from jupiter.core.common.sub.tags.root import TagDomain -from jupiter.core.common.sub.tags.sub.link.root import TagLinkRepository +from jupiter.core.common.sub.notes.root import Note +from jupiter.core.common.sub.publish.sub.entity.root import PublishEntity from jupiter.core.common.sub.tags.sub.tag.root import Tag from jupiter.core.config import ( JupiterLoggedInReadonlyContext, @@ -16,11 +12,7 @@ from jupiter.core.docs.sub.dir.root import Dir from jupiter.core.docs.sub.doc.root import Doc from jupiter.core.features import WorkspaceFeature -from jupiter.core.named_entity_tag import NamedEntityTag from jupiter.framework.base.entity_id import EntityId -from jupiter.framework.base.entity_link import EntityLink -from jupiter.framework.entity import NoFilter -from jupiter.framework.errors import InputValidationError from jupiter.framework.storage.repository import DomainUnitOfWork from jupiter.framework.use_case import readonly_use_case from jupiter.framework.use_case_io import ( @@ -65,6 +57,7 @@ class DirLoadResult(UseCaseResultBase): dir: Dir entries: list[DirLoadResultEntry] subdirs: list[DirLoadSubdirEntry] + publish_entity: PublishEntity | None @readonly_use_case(WorkspaceFeature.DOCS, exclude_component=[AppCore.CLI]) @@ -80,6 +73,8 @@ async def _perform_transactional_read( args: DirLoadArgs, ) -> DirLoadResult: """Execute the command's action.""" + from jupiter.core.docs.sub.dir.service.load import DirLoadService + allow_archived = args.allow_archived or False workspace = context.workspace doc_collection = await uow.get_for(DocCollection).load_by_parent( @@ -90,118 +85,13 @@ async def _perform_transactional_read( args.ref_id, allow_archived=allow_archived, ) - if dir_entity.doc_collection.ref_id != doc_collection.ref_id: - raise InputValidationError("Directory is not in this workspace.") - - subdirs_raw = await uow.get_for(Dir).find_all_generic( - parent_ref_id=doc_collection.ref_id, - allow_archived=allow_archived, - parent_dir_ref_id=[dir_entity.ref_id], - ) - subdirs_sorted = sorted(subdirs_raw, key=lambda d: str(d.name)) - docs = await uow.get_for(Doc).find_all_generic( - parent_ref_id=doc_collection.ref_id, + return await DirLoadService().do_it( + uow, + workspace.ref_id, + doc_collection.ref_id, + dir_entity, allow_archived=allow_archived, - ref_id=args.filter_ref_ids or NoFilter(), - parent_dir_ref_id=[dir_entity.ref_id], - ) - - note_collection = await uow.get_for(NoteCollection).load_by_parent( - workspace.ref_id - ) - notes = await uow.get(NoteRepository).find_all_for_note_collection( - note_collection_ref_id=note_collection.ref_id, - allow_archived=True, - filter_owners=[ - EntityLink.std(NamedEntityTag.DOC.value, rid) - for rid in [d.ref_id for d in docs] - ], - ) - notes_by_doc_ref_id: dict[EntityId, Note] = {} - for n in notes: - notes_by_doc_ref_id[n.owner.ref_id] = n - - tags_domain = await uow.get_for(TagDomain).load_by_parent(workspace.ref_id) - doc_tag_links = await uow.get(TagLinkRepository).find_all_generic( - parent_ref_id=tags_domain.ref_id, - allow_archived=False, - owner=[EntityLink.std(NamedEntityTag.DOC.value, d.ref_id) for d in docs], - ) - doc_tag_links_by_doc_ref_id = { - cast(EntityId, tl.owner.ref_id): tl for tl in doc_tag_links - } - dir_tag_links = ( - await uow.get(TagLinkRepository).find_all_generic( - parent_ref_id=tags_domain.ref_id, - allow_archived=False, - owner=[ - EntityLink.std(NamedEntityTag.DIR.value, sd.ref_id) - for sd in subdirs_sorted - ], - ) - if subdirs_sorted - else [] - ) - dir_tag_links_by_dir_ref_id = { - cast(EntityId, tl.owner.ref_id): tl for tl in dir_tag_links - } - all_tag_ref_ids: list[EntityId] = [] - for tl in doc_tag_links: - all_tag_ref_ids.extend(tl.ref_ids) - for tl in dir_tag_links: - all_tag_ref_ids.extend(tl.ref_ids) - if all_tag_ref_ids: - all_tags = await uow.get_for(Tag).find_all_generic( - parent_ref_id=tags_domain.ref_id, - allow_archived=False, - ref_id=list(set(all_tag_ref_ids)), - ) - all_tags_by_ref_id = {t.ref_id: t for t in all_tags} - else: - all_tags_by_ref_id = {} - - entries: list[DirLoadResultEntry] = [] - for doc in docs: - note = notes_by_doc_ref_id.get(doc.ref_id) - if note is None: - raise InputValidationError( - f"Doc {doc.ref_id} has no associated note.", - ) - entries.append( - DirLoadResultEntry( - doc=doc, - tags=( - [ - all_tags_by_ref_id[rid] - for rid in doc_tag_links_by_doc_ref_id[doc.ref_id].ref_ids - if rid in all_tags_by_ref_id - ] - if doc.ref_id in doc_tag_links_by_doc_ref_id - else [] - ), - note=note, - ) - ) - - subdir_entries = [ - DirLoadSubdirEntry( - dir=sd, - tags=( - [ - all_tags_by_ref_id[rid] - for rid in dir_tag_links_by_dir_ref_id[sd.ref_id].ref_ids - if rid in all_tags_by_ref_id - ] - if sd.ref_id in dir_tag_links_by_dir_ref_id - else [] - ), - ) - for sd in subdirs_sorted - ] - - return DirLoadResult( - dir=dir_entity, - entries=entries, - subdirs=subdir_entries, + filter_ref_ids=args.filter_ref_ids, + include_publish_entity=True, ) diff --git a/src/core/jupiter/core/docs/sub/dir/use_case/load_public.py b/src/core/jupiter/core/docs/sub/dir/use_case/load_public.py new file mode 100644 index 000000000..1dc8528ec --- /dev/null +++ b/src/core/jupiter/core/docs/sub/dir/use_case/load_public.py @@ -0,0 +1,44 @@ +"""Guest readonly use case for loading a published directory.""" + +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.docs.sub.dir.service.load import DirLoadService +from jupiter.core.docs.sub.dir.service.published import DirPublishedLoadService +from jupiter.core.docs.sub.dir.use_case.load import DirLoadResult +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class DirLoadPublicArgs(UseCaseArgsBase): + """DirLoadPublic args.""" + + external_id: PublishExternalId + + +class DirLoadPublicUseCase( + JupiterGuestReadonlyUseCase[DirLoadPublicArgs, DirLoadResult] +): + """Load a published directory by publish external id.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: DirLoadPublicArgs, + ) -> DirLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + published = await DirPublishedLoadService().load_context( + uow, args.external_id + ) + + return await DirLoadService().do_it( + uow, + published.doc_collection.workspace.ref_id, + published.doc_collection.ref_id, + published.published_dir, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/docs/sub/dir/use_case/load_public_from_dir.py b/src/core/jupiter/core/docs/sub/dir/use_case/load_public_from_dir.py new file mode 100644 index 000000000..8fbd26034 --- /dev/null +++ b/src/core/jupiter/core/docs/sub/dir/use_case/load_public_from_dir.py @@ -0,0 +1,50 @@ +"""Guest readonly use case for loading a subdirectory via a published directory.""" + +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.docs.sub.dir.service.load import DirLoadService +from jupiter.core.docs.sub.dir.service.published import DirPublishedLoadService +from jupiter.core.docs.sub.dir.use_case.load import DirLoadResult +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class DirLoadPublicFromDirArgs(UseCaseArgsBase): + """DirLoadPublicFromDir args.""" + + external_id: PublishExternalId + ref_id: EntityId + + +class DirLoadPublicFromDirUseCase( + JupiterGuestReadonlyUseCase[DirLoadPublicFromDirArgs, DirLoadResult] +): + """Load a subdirectory through a published directory.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: DirLoadPublicFromDirArgs, + ) -> DirLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + service = DirPublishedLoadService() + published = await service.load_context(uow, args.external_id) + dir_entity = await service.load_dir_under_published_root( + uow, + published, + args.ref_id, + ) + + return await DirLoadService().do_it( + uow, + published.doc_collection.workspace.ref_id, + published.doc_collection.ref_id, + dir_entity, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/core/jupiter/core/docs/sub/doc/use_case/load_public_from_dir.py b/src/core/jupiter/core/docs/sub/doc/use_case/load_public_from_dir.py new file mode 100644 index 000000000..a8977b45e --- /dev/null +++ b/src/core/jupiter/core/docs/sub/doc/use_case/load_public_from_dir.py @@ -0,0 +1,49 @@ +"""Guest readonly use case for loading a doc via a published directory.""" + +from jupiter.core.common.sub.publish.sub.entity.external_id import PublishExternalId +from jupiter.core.config import ( + JupiterGuestReadonlyContext, + JupiterGuestReadonlyUseCase, +) +from jupiter.core.docs.sub.dir.service.published import DirPublishedLoadService +from jupiter.core.docs.sub.doc.service.load import DocLoadResult, DocLoadService +from jupiter.framework.base.entity_id import EntityId +from jupiter.framework.use_case_io import UseCaseArgsBase, use_case_args + + +@use_case_args +class DocLoadPublicFromDirArgs(UseCaseArgsBase): + """DocLoadPublicFromDir args.""" + + external_id: PublishExternalId + dir_ref_id: EntityId + ref_id: EntityId + + +class DocLoadPublicFromDirUseCase( + JupiterGuestReadonlyUseCase[DocLoadPublicFromDirArgs, DocLoadResult] +): + """Load a doc through a published directory.""" + + async def _execute( + self, + context: JupiterGuestReadonlyContext, + args: DocLoadPublicFromDirArgs, + ) -> DocLoadResult: + """Execute the use case.""" + async with self._ports.domain_storage_engine.get_unit_of_work() as uow: + service = DirPublishedLoadService() + published = await service.load_context(uow, args.external_id) + doc = await service.load_doc_under_published_dir( + uow, + published, + args.dir_ref_id, + args.ref_id, + ) + + return await DocLoadService().do_it( + uow, + doc, + allow_archived=False, + include_publish_entity=False, + ) diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/webui/app/routes/app/public/published/$externalId.tsx index 12ec720b0..3facf2b7e 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/$externalId.tsx @@ -39,7 +39,9 @@ function publishedEntityLocation(externalId: string, owner: string): string { case NamedEntityTag.METRIC_ENTRY: return `/app/public/published/metric/entry/${externalId}`; case NamedEntityTag.DOC: - return `/app/public/published/doc/${externalId}`; + return `/app/public/published/doc/doc/${externalId}`; + case NamedEntityTag.DIR: + return `/app/public/published/doc/dir/${externalId}`; case NamedEntityTag.PERSON: return `/app/public/published/person/${externalId}`; case NamedEntityTag.HABIT: diff --git a/src/webui/app/routes/app/public/published/doc/dir/$externalId.dir/$dirId.tsx b/src/webui/app/routes/app/public/published/doc/dir/$externalId.dir/$dirId.tsx new file mode 100644 index 000000000..0ed60e1ea --- /dev/null +++ b/src/webui/app/routes/app/public/published/doc/dir/$externalId.dir/$dirId.tsx @@ -0,0 +1,93 @@ +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import type { Tag } from "@jupiter/webapi-client"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { parseEntityLinkStd } from "@jupiter/core/common/entity-link"; +import { PublishedDocDirPanel } from "@jupiter/core/docs/component/published-doc-dir-panel"; +import { makeTrunkErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), + dirId: z.string(), +}); + +export const handle = { + displayType: DisplayType.TRUNK, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + const { externalId, dirId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + const [dirLoad, publishEntityLoad] = await Promise.all([ + apiClient.docs.dirLoadPublicFromDir({ + external_id: externalId, + ref_id: dirId, + }), + apiClient.publish.publishEntityLoadByExternalId({ + external_id: externalId, + }), + ]); + + const publishedRootDirRefId = parseEntityLinkStd( + publishEntityLoad.publish_entity.owner, + ).refId; + + const basePath = `/app/public/published/doc/dirtree/${externalId}`; + const returnLocation = + dirId === publishedRootDirRefId + ? "/app" + : dirLoad.dir.parent_dir_ref_id === publishedRootDirRefId + ? `${basePath}/${publishedRootDirRefId}` + : `${basePath}/${dirLoad.dir.parent_dir_ref_id}`; + + return json({ + externalId, + dirLoad, + publishedRootDirRefId, + allTags: collectTagsFromDirLoad(dirLoad), + returnLocation, + }); + } catch (error) { + handlePublishedLoaderError(error); + } +} + +export default function PublishedDocDirChild() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + + return ( + <PublishedDocDirPanel + externalId={loaderData.externalId} + publishedRootDirRefId={loaderData.publishedRootDirRefId} + dirLoad={loaderData.dirLoad} + allTags={loaderData.allTags} + returnLocation={loaderData.returnLocation} + /> + ); +} + +export const ErrorBoundary = makeTrunkErrorBoundary("/app", { + error: () => + `There was an error loading the published folder! Please try again!`, +}); + +function collectTagsFromDirLoad(dirLoad: { + entries: Array<{ tags: Tag[] }>; + subdirs: Array<{ tags: Tag[] }>; +}): Tag[] { + const byRefId = new Map<string, Tag>(); + for (const entry of [...dirLoad.subdirs, ...dirLoad.entries]) { + for (const tag of entry.tags) { + byRefId.set(tag.ref_id, tag); + } + } + return [...byRefId.values()]; +} diff --git a/src/webui/app/routes/app/public/published/doc/dir/$externalId.tsx b/src/webui/app/routes/app/public/published/doc/dir/$externalId.tsx new file mode 100644 index 000000000..dda860062 --- /dev/null +++ b/src/webui/app/routes/app/public/published/doc/dir/$externalId.tsx @@ -0,0 +1,40 @@ +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { parseEntityLinkStd } from "@jupiter/core/common/entity-link"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; + +const ParamsSchema = z.object({ + externalId: z.string(), +}); + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + const { externalId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + const result = await apiClient.publish.publishEntityLoadByExternalId({ + external_id: externalId, + }); + + const { refId } = parseEntityLinkStd(result.publish_entity.owner); + + return redirect(`/app/public/published/doc/dirtree/${externalId}/${refId}`); + } catch (error) { + handlePublishedLoaderError(error); + } +} + +export default function PublishedDocDirRedirect() { + return null; +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => `Could not find published folder ${params.externalId}!`, + error: (params) => + `There was an error loading published folder ${params.externalId}! Please try again!`, +}); diff --git a/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId.tsx b/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId.tsx new file mode 100644 index 000000000..8f7d29a2b --- /dev/null +++ b/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId.tsx @@ -0,0 +1,94 @@ +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import type { Tag } from "@jupiter/webapi-client"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { parseEntityLinkStd } from "@jupiter/core/common/entity-link"; +import { PublishedDocDirPanel } from "@jupiter/core/docs/component/published-doc-dir-panel"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), + dirId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAF, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + const { externalId, dirId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + const [dirLoad, publishEntityLoad] = await Promise.all([ + apiClient.docs.dirLoadPublicFromDir({ + external_id: externalId, + ref_id: dirId, + }), + apiClient.publish.publishEntityLoadByExternalId({ + external_id: externalId, + }), + ]); + + const publishedRootDirRefId = parseEntityLinkStd( + publishEntityLoad.publish_entity.owner, + ).refId; + + const basePath = `/app/public/published/doc/dirtree/${externalId}`; + const returnLocation = + dirId === publishedRootDirRefId + ? "/app" + : dirLoad.dir.parent_dir_ref_id === publishedRootDirRefId + ? `${basePath}/${publishedRootDirRefId}` + : `${basePath}/${dirLoad.dir.parent_dir_ref_id}`; + + return json({ + externalId, + dirLoad, + publishedRootDirRefId, + allTags: collectTagsFromDirLoad(dirLoad), + returnLocation, + }); + } catch (error) { + handlePublishedLoaderError(error); + } +} + +export default function PublishedDocDirView() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + + return ( + <PublishedDocDirPanel + externalId={loaderData.externalId} + publishedRootDirRefId={loaderData.publishedRootDirRefId} + dirLoad={loaderData.dirLoad} + allTags={loaderData.allTags} + returnLocation={loaderData.returnLocation} + /> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { + notFound: (params) => `Could not find published folder ${params.dirId}!`, + error: (params) => + `There was an error loading published folder ${params.dirId}! Please try again!`, +}); + +function collectTagsFromDirLoad(dirLoad: { + entries: Array<{ tags: Tag[] }>; + subdirs: Array<{ tags: Tag[] }>; +}): Tag[] { + const byRefId = new Map<string, Tag>(); + for (const entry of [...dirLoad.subdirs, ...dirLoad.entries]) { + for (const tag of entry.tags) { + byRefId.set(tag.ref_id, tag); + } + } + return [...byRefId.values()]; +} diff --git a/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId/$docId.tsx b/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId/$docId.tsx new file mode 100644 index 000000000..86ba30838 --- /dev/null +++ b/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId/$docId.tsx @@ -0,0 +1,94 @@ +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { z } from "zod"; +import { parseParams } from "zodix"; +import { entityLinkStd } from "@jupiter/core/common/entity-link"; +import { TagsEditor } from "@jupiter/core/common/sub/tags/component/tags-editor"; +import { DocEditor } from "@jupiter/core/docs/component/editor"; +import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; +import { LeafPanel } from "@jupiter/core/infra/component/layout/leaf-panel"; +import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; + +import { getGuestApiClient } from "~/api-clients.server"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; + +const ParamsSchema = z.object({ + externalId: z.string(), + dirId: z.string(), + docId: z.string(), +}); + +export const handle = { + displayType: DisplayType.LEAFLET, +}; + +export async function loader({ request, params }: LoaderFunctionArgs) { + try { + const { externalId, dirId, docId } = parseParams(params, ParamsSchema); + const apiClient = await getGuestApiClient(request); + + const result = await apiClient.docs.docLoadPublicFromDir({ + external_id: externalId, + dir_ref_id: dirId, + ref_id: docId, + }); + + return json({ + externalId, + dirId, + doc: result.doc, + note: result.note, + tags: result.tags ?? [], + }); + } catch (error) { + handlePublishedLoaderError(error); + } +} + +export default function PublishedDocFromDir() { + const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); + const { doc, note, tags, externalId, dirId } = loaderData; + + return ( + <LeafPanel + key={`published-doc-${doc.ref_id}`} + fakeKey={`published-doc-${doc.ref_id}`} + isLeaflet + inputsEnabled={false} + entityNotEditable={true} + returnLocation={`/app/public/published/doc/dirtree/${externalId}/${dirId}`} + > + <SectionCard title="Doc"> + <DocEditor + initialDoc={doc} + initialNote={note} + inputsEnabled={false} + parentDirRefId={doc.parent_dir_ref_id} + rightOfName={ + <TagsEditor + name="tags" + allTags={tags} + defaultValue={tags.map((tag) => tag.ref_id)} + inputsEnabled={false} + owner={entityLinkStd(NamedEntityTag.DOC, doc.ref_id)} + /> + } + /> + </SectionCard> + </LeafPanel> + ); +} + +export const ErrorBoundary = makeLeafErrorBoundary( + (params) => + `/app/public/published/doc/dirtree/${params.externalId}/${params.dirId}`, + ParamsSchema, + { + notFound: (params) => `Could not find published doc ${params.docId}!`, + error: (params) => + `There was an error loading published doc ${params.docId}! Please try again!`, + }, +); diff --git a/src/webui/app/routes/app/public/published/doc/$externalId.tsx b/src/webui/app/routes/app/public/published/doc/doc/$externalId.tsx similarity index 100% rename from src/webui/app/routes/app/public/published/doc/$externalId.tsx rename to src/webui/app/routes/app/public/published/doc/doc/$externalId.tsx diff --git a/src/webui/app/routes/app/workspace/docs/$dirId.tsx b/src/webui/app/routes/app/workspace/docs/$dirId.tsx index 0e1b7b6e4..864bf930b 100644 --- a/src/webui/app/routes/app/workspace/docs/$dirId.tsx +++ b/src/webui/app/routes/app/workspace/docs/$dirId.tsx @@ -1,14 +1,15 @@ import { ApiError, DocsHelpSubject, + NamedEntityTag, type DirLoadResultEntry, type DirLoadSubdirEntry, type Tag, } from "@jupiter/webapi-client"; -import type { LoaderFunctionArgs } from "@remix-run/node"; -import { json } from "@remix-run/node"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; +import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; -import { Outlet } from "@remix-run/react"; +import { Form, Outlet } from "@remix-run/react"; import { CreateNewFolder as CreateNewFolderIcon, Settings as SettingsIcon, @@ -19,8 +20,9 @@ import { styled } from "@mui/material/styles"; import { ReasonPhrases, StatusCodes } from "http-status-codes"; import { useContext, useMemo, useState } from "react"; import { z } from "zod"; -import { parseParams } from "zodix"; +import { parseForm, parseParams } from "zodix"; import { isDirRoot } from "@jupiter/core/docs/sub/dir/root"; +import { PublishPanel } from "@jupiter/core/common/sub/publish/components/publish-panel"; import { EntityNameOneLineComponent } from "@jupiter/core/common/component/entity-name"; import { EntityNoNothingCard } from "@jupiter/core/infra/component/entity-no-nothing-card"; import { @@ -58,6 +60,21 @@ const ParamsSchema = z.object({ dirId: z.string(), }); +const UpdateFormSchema = z.discriminatedUnion("intent", [ + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), +]); + enum DocsSortOrder { CREATED_ASC = "created_asc", CREATED_DESC = "created_desc", @@ -90,6 +107,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return json({ dirLoad, allTags: allTags.tags, + publishEntity: dirLoad.publish_entity ?? null, }); } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.NOT_FOUND) { @@ -103,6 +121,35 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export async function action({ request, params }: ActionFunctionArgs) { + const apiClient = await getLoggedInApiClient(request); + const { dirId } = parseParams(params, ParamsSchema); + const form = await parseForm(request, UpdateFormSchema); + + switch (form.intent) { + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + break; + } + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + break; + } + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + break; + } + } + + return redirect(`/app/workspace/docs/${dirId}`); +} + export const shouldRevalidate: ShouldRevalidateFunction = standardShouldRevalidate; @@ -226,6 +273,16 @@ export default function DocsInFolder() { } > <NestingAwareBlock shouldHide={shouldShowALeaf}> + <Form method="post"> + <PublishPanel + entityType={NamedEntityTag.DIR} + entityRefId={dirId} + topLevelInfo={topLevelInfo} + inputsEnabled={true} + publishEntity={loaderData.publishEntity} + /> + </Form> + {listIsEmpty && !showParentLink && ( <EntityNoNothingCard title="You Have To Start Somewhere" From 53db495dddf983515fa2dd595a885f8ea4b57677 Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Mon, 8 Jun 2026 23:40:11 +0300 Subject: [PATCH 14/21] Some things --- src/docs/material/concepts/calendar.md | 3 +++ src/docs/material/concepts/overview.md | 2 ++ src/docs/material/features.md | 1 + src/docs/mkdocs.yml | 2 ++ tasks/build/stats-over-time.sh | 8 ++++---- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/docs/material/concepts/calendar.md b/src/docs/material/concepts/calendar.md index 750aa0352..0d2c1e21e 100644 --- a/src/docs/material/concepts/calendar.md +++ b/src/docs/material/concepts/calendar.md @@ -83,6 +83,9 @@ When a client calls this URL, Thrive returns an `.ics` calendar that contains: This endpoint is designed for read-only sharing. Anyone with the link can read the exported schedule. +To share a schedule stream as a read-only **Thrive web page** (not an iCal +feed), see [Publish](publish.md). + ## The Calendar The calendar is a representation of your current events. You can view it as a diff --git a/src/docs/material/concepts/overview.md b/src/docs/material/concepts/overview.md index 0c166f644..998d807e7 100644 --- a/src/docs/material/concepts/overview.md +++ b/src/docs/material/concepts/overview.md @@ -51,6 +51,8 @@ As a quick reference, here is the list of the more important concepts: Slack, GMail, Outlook, generic email, etc. Done in a one-way fashion via these tools pushing work into Thrive. +* _Publish_: optional read-only web links for sharing selected entities with + people who do not use Thrive. The rest of the document will cover each of these in greater detail. diff --git a/src/docs/material/features.md b/src/docs/material/features.md index 86416e08b..c89575930 100644 --- a/src/docs/material/features.md +++ b/src/docs/material/features.md @@ -48,6 +48,7 @@ others and give an extra boost of usefulness to Thrive. | Home Page | ✅ | ✅ | ✅ | | Gamification | ✅ | ✅ | ✅ | | Search | ✅ | ✅ | ✅ | +| Publish | ✅ | ✅ | ❌ | | Reporting | ✅ | ✅ | ✅ | | Stats | ✅ | ✅ | ✅ | | Pomodor Timer | ✅ | ✅ | ✅ | diff --git a/src/docs/mkdocs.yml b/src/docs/mkdocs.yml index f617a4928..ea7681e8f 100644 --- a/src/docs/mkdocs.yml +++ b/src/docs/mkdocs.yml @@ -36,6 +36,7 @@ nav: - 'Workspace': 'concepts/workspaces.md' - 'Feature Flags': 'concepts/feature-flags.md' - 'Home Page': 'concepts/home-page.md' + - 'Publish': 'concepts/publish.md' - 'Core Entities': - 'Inbox Tasks': 'concepts/core-entities/inbox-tasks.md' - 'Events': 'concepts/core-entities/events.md' @@ -79,6 +80,7 @@ nav: - 'Installation': 'how-tos/install.md' - 'Self-Hosting': 'how-tos/self-hosting.md' - 'Do Garbage Collection': 'how-tos/do-garbage-collection.md' + - 'Share an Entity': 'how-tos/share-an-entity.md' - 'Recover Your Account': 'how-tos/recover-your-account.md' - Roadmap: 'roadmap.md' - Releases: diff --git a/tasks/build/stats-over-time.sh b/tasks/build/stats-over-time.sh index a2acfeb82..c483a49f7 100755 --- a/tasks/build/stats-over-time.sh +++ b/tasks/build/stats-over-time.sh @@ -97,12 +97,12 @@ for sha in "${commits_ordered[@]}"; do git checkout --quiet "$sha" # Run cloc on whichever directories exist at this commit - cloc_dirs="" + cloc_dirs=() for d in docs/ infra/ itests/ src/ scripts/ jupiter/ tests/ migrations/ tasks/; do - [[ -d "$d" ]] && cloc_dirs="$cloc_dirs $d" + [[ -d "$d" ]] && cloc_dirs+=("$d") done - if [[ -z "$cloc_dirs" ]]; then + if [[ ${#cloc_dirs[@]} -eq 0 ]]; then log info "No source directories found for $short_sha, skipping" continue fi @@ -110,7 +110,7 @@ for sha in "${commits_ordered[@]}"; do total_loc=$(cloc \ --exclude-dir="node_modules,.build-cache,build,public,.mypy_cache,ios,android" \ --not-match-f="(pnpm-lock.json|uv.lock|.hcl)" \ - "$cloc_dirs" \ + "${cloc_dirs[@]}" \ 2>/dev/null \ | grep '^SUM' \ | awk '{print $NF}') || true From 3bc5381e87f6b832b55e8de7a6c235be6ba01d5f Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Tue, 9 Jun 2026 19:47:33 +0300 Subject: [PATCH 15/21] Docs --- src/docs/material/concepts/publish.md | 96 ++++++++++++++++++++ src/docs/material/how-tos/share-an-entity.md | 58 ++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 src/docs/material/concepts/publish.md create mode 100644 src/docs/material/how-tos/share-an-entity.md diff --git a/src/docs/material/concepts/publish.md b/src/docs/material/concepts/publish.md new file mode 100644 index 000000000..25cff80e2 --- /dev/null +++ b/src/docs/material/concepts/publish.md @@ -0,0 +1,96 @@ +# Publish + +Publish lets you share a **read-only** view of selected workspace entities with +people who do not have a Thrive account. You stay in control: sharing is +opt-in, you can prepare a link before it goes live, and you can turn it off +again without deleting the underlying entity. + +This is different from [schedule exports](calendar.md#schedule-exports), which +expose calendar streams as an iCal feed for other calendar apps. Publish is +about sharing Thrive entities themselves—habits, docs, persons, and so +on—as a web page anyone with the link can open. + +## What You Can Publish + +Many entity types support publish. The common ones include: + +* [Todos](todos.md) +* [Time plans](time-plans.md) +* Calendar [schedule streams](calendar.md) and their events +* [Habits](habits.md) and [chores](chores.md) +* [Big plans](big-plans.md) +* Docs and doc folders +* [Journals](journals.md) +* [Vacations](vacations.md) +* [Smart lists](smart-lists.md) and smart list items +* [Metrics](metrics.md) and metric entries +* [Persons](prm/persons.md) in the PRM + +Not every surface in the workspace is publishable. If an entity supports +publish, the web app exposes controls for it (see below). + +## Draft And Active + +Each published entity has a **status**: + +* **Draft** — you have created a share link, but guests cannot open it yet. + Useful while you check what will be visible. +* **Active** — the public link works. Anyone with the URL sees a read-only + view. + +Moving back to **draft** immediately revokes guest access. The link stays the +same if you activate again later. + +## Where To Manage Publish + +How you open the publish controls depends on what you are sharing: + +* **Most entities** (habits, chores, persons, individual docs, big plans, + etc.) — open the entity in the web app and use the **globe** button in the + leaf panel toolbar. That switches the panel to the publish section. +* **Smart lists** — use the globe button on the smart list branch view (the + list of items). +* **Doc folders** — open the folder in Docs. A **Publish** section appears at + the top of the folder listing. +* **All publish records** — open **Core → Publish** in the left sidebar. This + lists every publish entity in the workspace and lets you open one to see its + status and public URL. + +## The Public Link + +When a publish record exists, Thrive shows a **public URL**. That URL always +starts with your site’s public published path and a unique id. You can **copy** +it or **view** it in a new tab (when the publish is active). + +Guests do not sign in. They only see what you published, in read-only form. +They cannot browse the rest of your workspace. + +If a link stops working, it usually means the publish was moved back to draft, +the entity was removed, or the URL is wrong. Guests see a simple “not found” +page rather than any private data. + +## What Guests See For Composite Content + +Some published items include related content automatically. You do not need +separate publish records for every child item. + +* **Doc folder** — publishing a folder also shares its subfolders and docs. + Guests can browse the tree and open individual docs inside it. +* **Schedule stream** — guests can open the stream’s calendar and drill into + in-day and full-day events that belong to that stream. +* **Smart list** — guests can open items that belong to the published list. +* **Metric** — guests can open entries that belong to the published metric. + +Publishing a parent does not create extra publish entries for children; access +is implied by the tree you chose to share. + +## Security And Privacy + +Treat the public URL like an unlisted link: anyone who has it can view the +active publish. There is no separate password on the link. + +Only share entities you are comfortable showing read-only to others. Do not +put secrets in notes or fields on entities you plan to publish. + +For a step-by-step walkthrough in the web app, see [Share an +Entity](../how-tos/share-an-entity.md). diff --git a/src/docs/material/how-tos/share-an-entity.md b/src/docs/material/how-tos/share-an-entity.md new file mode 100644 index 000000000..37382590e --- /dev/null +++ b/src/docs/material/how-tos/share-an-entity.md @@ -0,0 +1,58 @@ +# Share an Entity + +This how-to walks through publishing a workspace entity so someone without a +Thrive account can view it read-only. The exact screen depends on the entity +type, but the **draft → active → copy link** flow is the same. + +For background, see [Publish](../concepts/publish.md). + +## With The Web App + +### From an entity (habit, person, doc, etc.) + +1. Sign in and open the entity you want to share (for example a [habit](../concepts/habits.md) + or [person](../concepts/prm/persons.md)). +2. Click the **globe** icon in the leaf panel toolbar. +3. In the **Publish** section, click **Publish**. Thrive creates a draft + publish record. +4. Click **Move to Active** when you are ready for others to open the link. +5. Copy the **public URL** and send it to whoever should view the entity. +6. Optional: open **View** to check the public page in a new tab. + +To stop sharing, open the publish section again and click **Move to Draft**. +The link stops working immediately. + +### From a doc folder + +1. Open **Docs** and navigate to the folder you want to share. +2. Use the **Publish** section at the top of the folder listing (not the globe + on a single doc). +3. Follow the same **Publish → Move to Active → copy URL** steps as above. + +Publishing a folder shares that folder and everything inside it—subfolders and +docs included. + +### From Core → Publish + +1. Open **Core → Publish** in the left sidebar. +2. Click a publish record to open its detail view, or create publish from the + entity first (steps above) and return here to manage many links in one place. +3. Use the publish panel to move between draft and active, or copy the public + URL. + +## What Recipients Do + +Recipients paste the public URL into a browser. They do not need an account. + +If the publish is **active**, they see a read-only version of the entity. If +it is still **draft**, or sharing was revoked, they see a not-found page. + +## Tips + +* Use **draft** while you edit the entity or check what will be visible, then + activate when ready. +* The public URL stays the same when you toggle draft and active, so you do + not need to resend the link after a temporary revoke. +* [Schedule exports](../concepts/calendar.md#schedule-exports) are separate: + those are iCal feeds for calendar apps, not the entity publish feature + described here. From dbbdb66b2a6fb62e5263a4cc9e20de840418e1d6 Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Tue, 9 Jun 2026 20:57:34 +0300 Subject: [PATCH 16/21] Added some more structures here for the pages --- src/webui/app/rendering/published-meta.ts | 145 ++++++++++++++++++ .../public/published/big-plan/$externalId.tsx | 17 +- .../public/published/chore/$externalId.tsx | 17 +- .../doc/dir/$externalId.dir/$dirId.tsx | 19 ++- .../doc/dirtree/$externalId/$dirId.tsx | 19 ++- .../doc/dirtree/$externalId/$dirId/$docId.tsx | 16 +- .../public/published/doc/doc/$externalId.tsx | 16 +- .../public/published/habit/$externalId.tsx | 17 +- .../public/published/journal/$externalId.tsx | 17 +- .../public/published/metric/$externalId.tsx | 23 ++- .../published/metric/$externalId/$entryId.tsx | 18 ++- .../published/metric/entry/$externalId.tsx | 18 ++- .../public/published/person/$externalId.tsx | 17 +- .../schedule-event-full-days/$externalId.tsx | 17 +- .../schedule-event-in-day/$externalId.tsx | 17 +- .../published/schedule-stream/$externalId.tsx | 22 ++- .../$externalId/full-days-event/$eventId.tsx | 17 +- .../$externalId/in-day-event/$eventId.tsx | 17 +- .../published/smart-list/$externalId.tsx | 19 ++- .../smart-list/$externalId/$itemId.tsx | 17 +- .../published/smart-list/item/$externalId.tsx | 17 +- .../published/time-plan/$externalId.tsx | 21 ++- .../published/todo-task/$externalId.tsx | 17 +- .../public/published/vacation/$externalId.tsx | 17 +- 24 files changed, 530 insertions(+), 27 deletions(-) create mode 100644 src/webui/app/rendering/published-meta.ts diff --git a/src/webui/app/rendering/published-meta.ts b/src/webui/app/rendering/published-meta.ts new file mode 100644 index 000000000..88ccd54a0 --- /dev/null +++ b/src/webui/app/rendering/published-meta.ts @@ -0,0 +1,145 @@ +import type { MetaFunction } from "@remix-run/node"; +import type { Note } from "@jupiter/webapi-client"; +import { publishOwnerEntityTagName } from "#/core/common/sub/publish/publish-owner-type-name"; +import { noteContentPreviewPlainText } from "#/core/common/sub/notes/note-content-plain-text"; + +export const PUBLISHED_SITE_NAME = "Thrive"; +export const PUBLISHED_DESCRIPTION_MAX_CHARS = 160; + +export type PublishedPageMeta = { + name: string; + entityType: string; + summary: string; + dateModified?: string | null; + canonicalUrl: string; + ogType: "article" | "website"; + /** Unlisted share links should stay noindex (default). */ + indexable: boolean; +}; + +export function publishedCanonicalUrl(request: Request): string { + const url = new URL(request.url); + url.search = ""; + url.hash = ""; + return url.href; +} + +export function publishedSummaryFromNote( + note: Note | null | undefined, +): string | undefined { + return ( + noteContentPreviewPlainText(note, PUBLISHED_DESCRIPTION_MAX_CHARS) ?? + undefined + ); +} + +function truncatePublishedDescription(text: string): string { + const normalized = text.trim().replace(/\s+/g, " "); + if (normalized.length <= PUBLISHED_DESCRIPTION_MAX_CHARS) { + return normalized; + } + return `${normalized.slice(0, PUBLISHED_DESCRIPTION_MAX_CHARS - 1).trimEnd()}…`; +} + +export function publishedDirListingSummary(dirLoad: { + entries: unknown[]; + subdirs: unknown[]; +}): string { + const parts: string[] = []; + if (dirLoad.subdirs.length > 0) { + parts.push(`${dirLoad.subdirs.length} folder(s)`); + } + if (dirLoad.entries.length > 0) { + parts.push(`${dirLoad.entries.length} doc(s)`); + } + if (parts.length === 0) { + return `Shared folder on ${PUBLISHED_SITE_NAME}`; + } + return parts.join(", "); +} + +export function buildPublishedPageMeta(args: { + request: Request; + entityType: string; + name: string; + summary?: string | null; + note?: Note | null; + dateModified?: string | null; + ogType?: "article" | "website"; + indexable?: boolean; +}): PublishedPageMeta { + const entityTypeLabel = publishOwnerEntityTagName(args.entityType); + const summary = + args.summary ?? + (args.note !== undefined + ? publishedSummaryFromNote(args.note) + : undefined) ?? + `Shared ${entityTypeLabel} on ${PUBLISHED_SITE_NAME}`; + + return { + name: args.name, + entityType: args.entityType, + summary: truncatePublishedDescription(summary), + dateModified: args.dateModified ?? null, + canonicalUrl: publishedCanonicalUrl(args.request), + ogType: args.ogType ?? "article", + indexable: args.indexable ?? false, + }; +} + +export function publishedMetaTitle(meta: PublishedPageMeta): string { + const entityTypeLabel = publishOwnerEntityTagName(meta.entityType); + return `${meta.name} · ${entityTypeLabel} · ${PUBLISHED_SITE_NAME}`; +} + +export function publishedMetaRobots(meta: PublishedPageMeta): string { + return meta.indexable ? "index,follow" : "noindex,nofollow"; +} + +export function metaDescriptorsForPublishedPage( + pageMeta: PublishedPageMeta | null | undefined, +) { + if (!pageMeta) { + return [{ title: `Not found · ${PUBLISHED_SITE_NAME}` }]; + } + + const title = publishedMetaTitle(pageMeta); + const description = pageMeta.summary; + const robots = publishedMetaRobots(pageMeta); + + const jsonLd: Record<string, unknown> = { + "@context": "https://schema.org", + "@type": "CreativeWork", + name: pageMeta.name, + description, + url: pageMeta.canonicalUrl, + }; + if (pageMeta.dateModified) { + jsonLd.dateModified = pageMeta.dateModified; + } + + return [ + { title }, + { name: "description", content: description }, + { tagName: "link", rel: "canonical", href: pageMeta.canonicalUrl }, + { name: "robots", content: robots }, + { property: "og:title", content: title }, + { property: "og:description", content: description }, + { property: "og:type", content: pageMeta.ogType }, + { property: "og:url", content: pageMeta.canonicalUrl }, + { property: "og:site_name", content: PUBLISHED_SITE_NAME }, + { name: "twitter:card", content: "summary" }, + { name: "twitter:title", content: title }, + { name: "twitter:description", content: description }, + { "script:ld+json": jsonLd }, + ]; +} + +export function createPublishedMetaFunction< + Loader extends (...args: never[]) => Promise<unknown>, +>(): MetaFunction<Loader> { + return ({ data }) => + metaDescriptorsForPublishedPage( + (data as { pageMeta?: PublishedPageMeta } | undefined)?.pageMeta, + ); +} diff --git a/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx b/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx index 6602664aa..94c3093ac 100644 --- a/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx @@ -1,6 +1,7 @@ import type { BigPlanLoadResult, InboxTask } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext, useMemo } from "react"; import { z } from "zod"; @@ -18,6 +19,10 @@ import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansio import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -48,6 +53,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.BIG_PLAN, + name: result.big_plan.name, + note: result.note, + dateModified: result.big_plan.last_modified_time, + }), bigPlan: result.big_plan, stats: result.stats, aspect: result.aspect, @@ -66,6 +78,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedBigPlan() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/chore/$externalId.tsx b/src/webui/app/routes/app/public/published/chore/$externalId.tsx index 09fb356e7..d52891587 100644 --- a/src/webui/app/routes/app/public/published/chore/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/chore/$externalId.tsx @@ -1,6 +1,7 @@ import type { InboxTask } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext, useMemo } from "react"; import { z } from "zod"; @@ -17,6 +18,10 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { ChoreEditor } from "@jupiter/core/chores/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -47,6 +52,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.CHORE, + name: result.chore.name, + note: result.note, + dateModified: result.chore.last_modified_time, + }), chore: result.chore, tags: result.tags ?? [], contacts: result.contacts ?? [], @@ -63,6 +75,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedChore() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/doc/dir/$externalId.dir/$dirId.tsx b/src/webui/app/routes/app/public/published/doc/dir/$externalId.dir/$dirId.tsx index 0ed60e1ea..c99d34c06 100644 --- a/src/webui/app/routes/app/public/published/doc/dir/$externalId.dir/$dirId.tsx +++ b/src/webui/app/routes/app/public/published/doc/dir/$externalId.dir/$dirId.tsx @@ -1,4 +1,5 @@ -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import type { Tag } from "@jupiter/webapi-client"; import { z } from "zod"; @@ -9,6 +10,11 @@ import { makeTrunkErrorBoundary } from "@jupiter/core/infra/component/error-boun import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, + publishedDirListingSummary, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -49,6 +55,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { : `${basePath}/${dirLoad.dir.parent_dir_ref_id}`; return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.DIR, + name: dirLoad.dir.name, + summary: publishedDirListingSummary(dirLoad), + dateModified: dirLoad.dir.last_modified_time, + ogType: "website", + }), externalId, dirLoad, publishedRootDirRefId, @@ -60,6 +74,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedDocDirChild() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); diff --git a/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId.tsx b/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId.tsx index 8f7d29a2b..f2f98f487 100644 --- a/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId.tsx +++ b/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId.tsx @@ -1,4 +1,5 @@ -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import type { Tag } from "@jupiter/webapi-client"; import { z } from "zod"; @@ -9,6 +10,11 @@ import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-bound import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, + publishedDirListingSummary, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -49,6 +55,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { : `${basePath}/${dirLoad.dir.parent_dir_ref_id}`; return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.DIR, + name: dirLoad.dir.name, + summary: publishedDirListingSummary(dirLoad), + dateModified: dirLoad.dir.last_modified_time, + ogType: "website", + }), externalId, dirLoad, publishedRootDirRefId, @@ -60,6 +74,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedDocDirView() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); diff --git a/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId/$docId.tsx b/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId/$docId.tsx index 86ba30838..746f581ee 100644 --- a/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId/$docId.tsx +++ b/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId/$docId.tsx @@ -1,5 +1,5 @@ import { NamedEntityTag } from "@jupiter/webapi-client"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { z } from "zod"; import { parseParams } from "zodix"; @@ -12,6 +12,10 @@ import { SectionCard } from "@jupiter/core/infra/component/section-card"; import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -37,6 +41,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.DOC, + name: result.doc.name, + note: result.note, + dateModified: result.doc.last_modified_time, + }), externalId, dirId, doc: result.doc, @@ -48,6 +59,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedDocFromDir() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const { doc, note, tags, externalId, dirId } = loaderData; diff --git a/src/webui/app/routes/app/public/published/doc/doc/$externalId.tsx b/src/webui/app/routes/app/public/published/doc/doc/$externalId.tsx index 2ed54d419..aaede469c 100644 --- a/src/webui/app/routes/app/public/published/doc/doc/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/doc/doc/$externalId.tsx @@ -1,5 +1,5 @@ import { NamedEntityTag } from "@jupiter/webapi-client"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { z } from "zod"; import { parseParams } from "zodix"; @@ -13,6 +13,10 @@ import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansio import { DocEditor } from "@jupiter/core/docs/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -34,6 +38,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.DOC, + name: result.doc.name, + note: result.note, + dateModified: result.doc.last_modified_time, + }), doc: result.doc, note: result.note, tags: result.tags ?? [], @@ -43,6 +54,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedDoc() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const { doc, note, tags } = loaderData; diff --git a/src/webui/app/routes/app/public/published/habit/$externalId.tsx b/src/webui/app/routes/app/public/published/habit/$externalId.tsx index dfa0912d9..d3ffac0a9 100644 --- a/src/webui/app/routes/app/public/published/habit/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/habit/$externalId.tsx @@ -1,6 +1,7 @@ import type { InboxTask } from "@jupiter/webapi-client"; import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { DateTime } from "luxon"; import { useContext, useMemo } from "react"; @@ -18,6 +19,10 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { HabitEditor } from "@jupiter/core/habits/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -60,6 +65,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.HABIT, + name: result.habit.name, + note: result.note, + dateModified: result.habit.last_modified_time, + }), habit: result.habit, tags: result.tags ?? [], contacts: result.contacts ?? [], @@ -79,6 +91,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedHabit() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/journal/$externalId.tsx b/src/webui/app/routes/app/public/published/journal/$externalId.tsx index 56064d7ca..4bd121da5 100644 --- a/src/webui/app/routes/app/public/published/journal/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/journal/$externalId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext } from "react"; import { z } from "zod"; @@ -16,6 +17,10 @@ import { allowUserChanges } from "@jupiter/core/journals/source"; import { ShowReport } from "@jupiter/core/report/component/show-report"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -37,6 +42,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.JOURNAL, + name: result.journal.name, + note: result.note, + dateModified: result.journal.last_modified_time, + }), journal: result.journal, journalStats: result.journal_stats, note: result.note, @@ -47,6 +59,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedJournal() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/metric/$externalId.tsx b/src/webui/app/routes/app/public/published/metric/$externalId.tsx index e9507c44f..d9b922c76 100644 --- a/src/webui/app/routes/app/public/published/metric/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/metric/$externalId.tsx @@ -1,9 +1,13 @@ import type { Contact, MetricEntry, Tag } from "@jupiter/webapi-client"; -import { DocsHelpSubject, MetricDirection } from "@jupiter/webapi-client"; +import { + DocsHelpSubject, + MetricDirection, + NamedEntityTag, +} from "@jupiter/webapi-client"; import { styled } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import { ResponsiveLine } from "@nivo/line"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Outlet } from "@remix-run/react"; import { AnimatePresence } from "framer-motion"; @@ -38,6 +42,10 @@ import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansio import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -82,6 +90,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.METRIC, + name: response.metric.name, + summary: `${response.metric_entries.length} entries`, + dateModified: response.metric.last_modified_time, + ogType: "website", + }), externalId, metric: response.metric, metricEntries: response.metric_entries, @@ -95,6 +111,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedMetric() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/metric/$externalId/$entryId.tsx b/src/webui/app/routes/app/public/published/metric/$externalId/$entryId.tsx index ab5fe6807..24e9850c3 100644 --- a/src/webui/app/routes/app/public/published/metric/$externalId/$entryId.tsx +++ b/src/webui/app/routes/app/public/published/metric/$externalId/$entryId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext } from "react"; import { z } from "zod"; @@ -11,8 +12,13 @@ import { SectionCard } from "@jupiter/core/infra/component/section-card"; import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { MetricEntryEditor } from "@jupiter/core/metrics/sub/entry/component/editor"; +import { metricEntryName } from "@jupiter/core/metrics/sub/entry/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -36,6 +42,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.METRIC_ENTRY, + name: metricEntryName(result.metric_entry), + note: result.note, + dateModified: result.metric_entry.last_modified_time, + }), externalId, metricEntry: result.metric_entry, tags: result.tags ?? [], @@ -47,6 +60,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedMetricEntryFromMetric() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/metric/entry/$externalId.tsx b/src/webui/app/routes/app/public/published/metric/entry/$externalId.tsx index f5e444e29..37f91dadc 100644 --- a/src/webui/app/routes/app/public/published/metric/entry/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/metric/entry/$externalId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext } from "react"; import { z } from "zod"; @@ -12,8 +13,13 @@ import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansion"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { MetricEntryEditor } from "@jupiter/core/metrics/sub/entry/component/editor"; +import { metricEntryName } from "@jupiter/core/metrics/sub/entry/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -35,6 +41,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.METRIC_ENTRY, + name: metricEntryName(result.metric_entry), + note: result.note, + dateModified: result.metric_entry.last_modified_time, + }), metricEntry: result.metric_entry, tags: result.tags ?? [], contacts: result.contacts ?? [], @@ -45,6 +58,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedMetricEntry() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/person/$externalId.tsx b/src/webui/app/routes/app/public/published/person/$externalId.tsx index f3d49856f..74cf6f3b9 100644 --- a/src/webui/app/routes/app/public/published/person/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/person/$externalId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext } from "react"; import { z } from "zod"; @@ -15,6 +16,10 @@ import { PersonEditor } from "@jupiter/core/prm/sub/person/component/editor"; import { OccasionStack } from "@jupiter/core/prm/sub/person/sub/occasion/components/stack"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -36,6 +41,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.PERSON, + name: result.person.name, + note: result.note, + dateModified: result.person.last_modified_time, + }), person: result.person, contact: result.contact, tags: result.tags ?? [], @@ -50,6 +62,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedPerson() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx b/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx index f292899cd..60138c313 100644 --- a/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext } from "react"; import { z } from "zod"; @@ -15,6 +16,10 @@ import { ScheduleEventFullDaysEditor } from "@jupiter/core/schedule/sub/event_fu import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_full_days/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -36,6 +41,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS, + name: result.schedule_event_full_days.name, + note: result.note, + dateModified: result.schedule_event_full_days.last_modified_time, + }), scheduleEventFullDays: result.schedule_event_full_days, timeEventFullDaysBlock: result.time_event_full_days_block, scheduleStream: result.schedule_stream, @@ -48,6 +60,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedScheduleEventFullDays() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx b/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx index 1a22ad616..f71991ae9 100644 --- a/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext, useMemo } from "react"; import { z } from "zod"; @@ -16,6 +17,10 @@ import { ScheduleEventInDayEditor } from "@jupiter/core/schedule/sub/event_in_da import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_in_day/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -37,6 +42,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.SCHEDULE_EVENT_IN_DAY, + name: result.schedule_event_in_day.name, + note: result.note, + dateModified: result.schedule_event_in_day.last_modified_time, + }), scheduleEventInDay: result.schedule_event_in_day, timeEventInDayBlock: result.time_event_in_day_block, scheduleStream: result.schedule_stream, @@ -49,6 +61,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedScheduleEventInDay() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/schedule-stream/$externalId.tsx b/src/webui/app/routes/app/public/published/schedule-stream/$externalId.tsx index c29864744..7c150dde2 100644 --- a/src/webui/app/routes/app/public/published/schedule-stream/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/schedule-stream/$externalId.tsx @@ -1,7 +1,11 @@ -import { AppPlatform, RecurringTaskPeriod } from "@jupiter/webapi-client"; +import { + AppPlatform, + NamedEntityTag, + RecurringTaskPeriod, +} from "@jupiter/webapi-client"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; import { Outlet, useSearchParams } from "@remix-run/react"; @@ -38,6 +42,10 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { inferPlatformAndDistribution } from "@jupiter/core/frontdoor.server"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; @@ -123,6 +131,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ]); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.SCHEDULE_STREAM, + name: streamResponse.schedule_stream.name, + dateModified: streamResponse.schedule_stream.last_modified_time, + ogType: "website", + }), externalId, scheduleStream: streamResponse.schedule_stream, date: query.date as string, @@ -140,6 +155,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export const shouldRevalidate: ShouldRevalidateFunction = standardShouldRevalidate; diff --git a/src/webui/app/routes/app/public/published/schedule-stream/$externalId/full-days-event/$eventId.tsx b/src/webui/app/routes/app/public/published/schedule-stream/$externalId/full-days-event/$eventId.tsx index 152f58c80..32e6e4605 100644 --- a/src/webui/app/routes/app/public/published/schedule-stream/$externalId/full-days-event/$eventId.tsx +++ b/src/webui/app/routes/app/public/published/schedule-stream/$externalId/full-days-event/$eventId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useSearchParams } from "@remix-run/react"; import { useContext } from "react"; @@ -15,6 +16,10 @@ import { ScheduleEventFullDaysEditor } from "@jupiter/core/schedule/sub/event_fu import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_full_days/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -41,6 +46,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS, + name: result.schedule_event_full_days.name, + note: result.note, + dateModified: result.schedule_event_full_days.last_modified_time, + }), externalId, scheduleEventFullDays: result.schedule_event_full_days, timeEventFullDaysBlock: result.time_event_full_days_block, @@ -54,6 +66,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedScheduleStreamFullDaysEvent() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/schedule-stream/$externalId/in-day-event/$eventId.tsx b/src/webui/app/routes/app/public/published/schedule-stream/$externalId/in-day-event/$eventId.tsx index d4bda13c7..a1706ec15 100644 --- a/src/webui/app/routes/app/public/published/schedule-stream/$externalId/in-day-event/$eventId.tsx +++ b/src/webui/app/routes/app/public/published/schedule-stream/$externalId/in-day-event/$eventId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useSearchParams } from "@remix-run/react"; import { useContext, useMemo } from "react"; @@ -16,6 +17,10 @@ import { ScheduleEventInDayEditor } from "@jupiter/core/schedule/sub/event_in_da import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_in_day/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -40,6 +45,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.SCHEDULE_EVENT_IN_DAY, + name: result.schedule_event_in_day.name, + note: result.note, + dateModified: result.schedule_event_in_day.last_modified_time, + }), externalId, scheduleEventInDay: result.schedule_event_in_day, timeEventInDayBlock: result.time_event_in_day_block, @@ -53,6 +65,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedScheduleStreamInDayEvent() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/smart-list/$externalId.tsx b/src/webui/app/routes/app/public/published/smart-list/$externalId.tsx index 690156897..a0529eec4 100644 --- a/src/webui/app/routes/app/public/published/smart-list/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/smart-list/$externalId.tsx @@ -1,6 +1,6 @@ import type { Contact, Tag } from "@jupiter/webapi-client"; -import { DocsHelpSubject } from "@jupiter/webapi-client"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { DocsHelpSubject, NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { Outlet } from "@remix-run/react"; import { AnimatePresence } from "framer-motion"; @@ -33,6 +33,10 @@ import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansio import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -74,6 +78,14 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.SMART_LIST, + name: response.smart_list.name, + summary: `${response.smart_list_items.length} items`, + dateModified: response.smart_list.last_modified_time, + ogType: "website", + }), externalId, smartList: response.smart_list, smartListItems: response.smart_list_items, @@ -87,6 +99,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedSmartList() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/smart-list/$externalId/$itemId.tsx b/src/webui/app/routes/app/public/published/smart-list/$externalId/$itemId.tsx index b541da8b7..bee7eff63 100644 --- a/src/webui/app/routes/app/public/published/smart-list/$externalId/$itemId.tsx +++ b/src/webui/app/routes/app/public/published/smart-list/$externalId/$itemId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext } from "react"; import { z } from "zod"; @@ -13,6 +14,10 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { SmartListItemEditor } from "@jupiter/core/smart_lists/sub/item/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -37,6 +42,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.SMART_LIST_ITEM, + name: result.item.name, + note: result.note, + dateModified: result.item.last_modified_time, + }), externalId, item: result.item, genericTags: result.generic_tags ?? [], @@ -48,6 +60,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedSmartListItemFromList() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/smart-list/item/$externalId.tsx b/src/webui/app/routes/app/public/published/smart-list/item/$externalId.tsx index 32e267ba9..e63a6942d 100644 --- a/src/webui/app/routes/app/public/published/smart-list/item/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/smart-list/item/$externalId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext } from "react"; import { z } from "zod"; @@ -14,6 +15,10 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { SmartListItemEditor } from "@jupiter/core/smart_lists/sub/item/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -35,6 +40,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.SMART_LIST_ITEM, + name: result.item.name, + note: result.note, + dateModified: result.item.last_modified_time, + }), item: result.item, genericTags: result.generic_tags ?? [], contacts: result.contacts ?? [], @@ -45,6 +57,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedSmartListItem() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx b/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx index d336aa626..39b7a624b 100644 --- a/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx @@ -4,8 +4,11 @@ import type { TimePlanActivity, TimePlanActivityDoneness, } from "@jupiter/webapi-client"; -import { TimePlanActivityFeasability } from "@jupiter/webapi-client"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { + TimePlanActivityFeasability, + NamedEntityTag, +} from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext, useMemo } from "react"; import { z } from "zod"; @@ -25,6 +28,10 @@ import { allowUserChanges } from "@jupiter/core/time_plans/source"; import { TimePlanListMergedActivities } from "@jupiter/core/time_plans/component/list-merged-activities"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -46,6 +53,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.TIME_PLAN, + name: result.time_plan.name, + note: result.note, + dateModified: result.time_plan.last_modified_time, + }), timePlan: result.time_plan, tags: result.tags ?? [], note: result.note, @@ -65,6 +79,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedTimePlan() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx b/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx index 2eeb06ad7..6f97e1798 100644 --- a/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext } from "react"; import { z } from "zod"; @@ -14,6 +15,10 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { TodoTaskPropertiesEditor } from "@jupiter/core/todo/components/properties-editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -35,6 +40,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.TODO_TASK, + name: result.inbox_task.name, + note: result.note, + dateModified: result.inbox_task.last_modified_time, + }), todoTask: result.todo_task, inboxTask: result.inbox_task, note: result.note ?? null, @@ -49,6 +61,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedTodoTask() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); diff --git a/src/webui/app/routes/app/public/published/vacation/$externalId.tsx b/src/webui/app/routes/app/public/published/vacation/$externalId.tsx index af7bf0965..bafb5f99a 100644 --- a/src/webui/app/routes/app/public/published/vacation/$externalId.tsx +++ b/src/webui/app/routes/app/public/published/vacation/$externalId.tsx @@ -1,5 +1,6 @@ import { Typography } from "@mui/material"; -import type { LoaderFunctionArgs } from "@remix-run/node"; +import { NamedEntityTag } from "@jupiter/webapi-client"; +import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { useContext } from "react"; import { z } from "zod"; @@ -14,6 +15,10 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { VacationEditor } from "@jupiter/core/vacations/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { + buildPublishedPageMeta, + metaDescriptorsForPublishedPage, +} from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; @@ -35,6 +40,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); return json({ + pageMeta: buildPublishedPageMeta({ + request, + entityType: NamedEntityTag.VACATION, + name: result.vacation.name, + note: result.note, + dateModified: result.vacation.last_modified_time, + }), vacation: result.vacation, note: result.note ?? null, timeEventBlock: result.time_event_block, @@ -46,6 +58,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +export const meta: MetaFunction<typeof loader> = ({ data }) => + metaDescriptorsForPublishedPage(data?.pageMeta); + export default function PublishedVacation() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); From 666400bf794fe7571469484ba04e5151208350f3 Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Wed, 10 Jun 2026 17:11:55 +0300 Subject: [PATCH 17/21] Moved to a new structure for a published service --- .eslintignore | 3 +- .eslintrc.js | 13 +- .gitignore | 2 + AGENTS.md | 3 +- docs/adrs/0010.entity-publish-mechanism.md | 14 +- .../jupiter_webapi_client/models/app_core.py | 1 + .../models/working_mem_load_current_entry.py | 38 ++++- gen/ts/webapi-client/gen/models/AppCore.ts | 1 + .../gen/models/WorkingMemLoadCurrentEntry.ts | 2 + infra/Config.infra | 1 + infra/self-hosted/compose.yaml | 27 ++++ infra/self-hosted/webui.conf | 12 ++ infra/self-hosted/webui.nodomain.conf | 12 ++ infra/terraform.tf | 31 +++- itests/webui/entities/big_plans.test.py | 4 +- itests/webui/entities/chores.test.py | 4 +- itests/webui/entities/docs.test.py | 14 +- itests/webui/entities/habits.test.py | 4 +- itests/webui/entities/journals.test.py | 4 +- itests/webui/entities/metrics.test.py | 12 +- itests/webui/entities/persons.test.py | 4 +- itests/webui/entities/schedules.test.py | 12 +- itests/webui/entities/smart_list.test.py | 14 +- itests/webui/entities/time_plans.test.py | 4 +- itests/webui/entities/todos.test.py | 4 +- itests/webui/entities/vacations.test.py | 4 +- mise.toml | 1 + mise.toml.hbs | 1 + pnpm-lock.yaml | 118 +++++++++++++++ pnpm-workspace.yaml | 1 + render.yaml | 59 ++++++++ src/Config.global | 1 + src/core/jupiter/core/app.py | 1 + .../google-oauth-redirect-state.server.ts | 9 +- .../auth/sub/google/oauth-state.server.ts | 29 ++-- .../jupiter/core/big_plans/component/card.tsx | 10 +- .../component/calendar-navigation.tsx | 2 +- .../common/sub/inbox_tasks/component/card.tsx | 10 +- .../sub/publish/components/publish-panel.tsx | 10 +- .../common/sub/publish/published-share-url.ts | 11 ++ src/core/jupiter/core/config-client.ts | 67 +-------- src/core/jupiter/core/config-server.ts | 106 +++++-------- .../component/published-doc-dir-panel.tsx | 2 +- .../core/infra/component/docs-help.tsx | 9 +- .../infra/component/progress-reporter.tsx | 6 +- .../infra/component/release-update-widget.tsx | 19 +-- .../core/infra/component/use-big-screen.ts | 16 +- .../core/infra/frontdoor-info-context.ts | 12 ++ .../core/infra/overdue-thresholds-context.ts | 15 ++ .../core/infra/service-links-context.ts | 15 ++ .../core/search/components/search-widget.tsx | 7 +- .../core/working_mem/use_case/load_current.py | 17 ++- src/published/Config.project | 4 + src/published/Dockerfile | 49 ++++++ src/published/README.md | 3 + src/published/app/api-clients.server.ts | 42 ++++++ src/published/app/entry.client.tsx | 50 +++++++ src/published/app/entry.server.tsx | 76 ++++++++++ src/published/app/logic/config.server.ts | 58 +++++++ src/published/app/logic/config.ts | 36 +++++ src/published/app/logic/navigation.ts | 13 ++ .../app/rendering/published-loader.server.ts | 0 .../app/rendering/published-meta.ts | 0 .../rendering/standard-should-revalidate.ts | 83 +++++++++++ .../use-loader-data-for-animation.ts | 14 ++ src/published/app/root.tsx | 120 +++++++++++++++ src/published/app/routes/healthz.tsx | 5 + .../app/routes/publish.tsx} | 85 ++++++++--- .../app/routes/publish}/$externalId.tsx | 36 ++--- .../routes/publish}/big-plan/$externalId.tsx | 4 +- .../app/routes/publish}/chore/$externalId.tsx | 4 +- .../doc/dir/$externalId.dir/$dirId.tsx | 4 +- .../routes/publish}/doc/dir/$externalId.tsx | 4 +- .../doc/dirtree/$externalId/$dirId.tsx | 6 +- .../doc/dirtree/$externalId/$dirId/$docId.tsx | 7 +- .../routes/publish}/doc/doc/$externalId.tsx | 4 +- .../app/routes/publish}/habit/$externalId.tsx | 4 +- .../routes/publish}/journal/$externalId.tsx | 4 +- .../routes/publish}/metric/$externalId.tsx | 6 +- .../publish}/metric/$externalId/$entryId.tsx | 6 +- .../publish}/metric/entry/$externalId.tsx | 4 +- .../routes/publish}/person/$externalId.tsx | 4 +- .../schedule-event-full-days/$externalId.tsx | 4 +- .../schedule-event-in-day/$externalId.tsx | 4 +- .../publish}/schedule-stream/$externalId.tsx | 12 +- .../$externalId/full-days-event/$eventId.tsx | 6 +- .../$externalId/in-day-event/$eventId.tsx | 6 +- .../publish}/smart-list/$externalId.tsx | 6 +- .../smart-list/$externalId/$itemId.tsx | 6 +- .../publish}/smart-list/item/$externalId.tsx | 4 +- .../routes/publish}/time-plan/$externalId.tsx | 4 +- .../routes/publish}/todo-task/$externalId.tsx | 4 +- .../routes/publish}/vacation/$externalId.tsx | 4 +- src/published/package.json | 59 ++++++++ src/published/package.mise.toml | 107 +++++++++++++ src/published/remix.config.js | 14 ++ src/published/remix.env.d.ts | 2 + src/published/tsconfig.json | 28 ++++ src/webui/Config.project | 1 + src/webui/app/api-clients.server.ts | 6 +- src/webui/app/logic/config.server.ts | 96 ++++++++++++ src/webui/app/logic/config.ts | 50 +++++++ src/webui/app/routes/app.tsx | 26 +++- src/webui/app/routes/app/index.tsx | 3 +- .../init/google/create-or-login-user.tsx | 23 ++- .../app/lifecycle/init/google/prepare.tsx | 11 +- .../app/lifecycle/init/google/ready.tsx | 12 +- .../app/lifecycle/init/local/create-user.tsx | 2 +- .../app/lifecycle/login/local/login.tsx | 2 +- .../app/routes/app/lifecycle/logout.server.ts | 6 +- .../calendar/schedule/export/$id.tsx | 4 +- .../routes/app/workspace/core/inbox-tasks.tsx | 4 +- .../app/routes/app/workspace/core/publish.tsx | 8 +- .../app/routes/app/workspace/manage-api.tsx | 4 +- .../app/routes/app/workspace/manage-mcp.tsx | 4 +- .../app/routes/app/workspace/working-mem.tsx | 82 +++++++++- src/webui/app/routes/pwa-manifest.tsx | 7 +- src/webui/app/routes/test-manifest.tsx | 8 +- src/webui/app/sessions.ts | 3 +- tasks/_common.sh | 141 +++++++++++++++--- tasks/_resources/pm2.config.ci.js.hbs | 21 +++ tasks/_resources/pm2.config.dev.js.hbs | 22 +++ tasks/build/api-clients.sh | 3 +- tasks/build/docker.sh | 3 +- tasks/generate-client-code.sh | 5 +- tasks/release/upload-docker.sh | 2 +- tasks/run/db/migrate.sh | 3 +- tasks/run/instance/list.sh | 3 +- tasks/run/srv.sh | 5 +- tasks/test/_common.sh | 16 +- tasks/test/int.sh | 3 +- tasks/test/matrix.sh | 8 +- 132 files changed, 1949 insertions(+), 451 deletions(-) create mode 100644 src/core/jupiter/core/common/sub/publish/published-share-url.ts create mode 100644 src/core/jupiter/core/infra/frontdoor-info-context.ts create mode 100644 src/core/jupiter/core/infra/overdue-thresholds-context.ts create mode 100644 src/core/jupiter/core/infra/service-links-context.ts create mode 100644 src/published/Config.project create mode 100644 src/published/Dockerfile create mode 100644 src/published/README.md create mode 100644 src/published/app/api-clients.server.ts create mode 100644 src/published/app/entry.client.tsx create mode 100644 src/published/app/entry.server.tsx create mode 100644 src/published/app/logic/config.server.ts create mode 100644 src/published/app/logic/config.ts create mode 100644 src/published/app/logic/navigation.ts rename src/{webui => published}/app/rendering/published-loader.server.ts (100%) rename src/{webui => published}/app/rendering/published-meta.ts (100%) create mode 100644 src/published/app/rendering/standard-should-revalidate.ts create mode 100644 src/published/app/rendering/use-loader-data-for-animation.ts create mode 100644 src/published/app/root.tsx create mode 100644 src/published/app/routes/healthz.tsx rename src/{webui/app/routes/app/public/published.tsx => published/app/routes/publish.tsx} (50%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/$externalId.tsx (66%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/big-plan/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/chore/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/doc/dir/$externalId.dir/$dirId.tsx (97%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/doc/dir/$externalId.tsx (88%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/doc/dirtree/$externalId/$dirId.tsx (96%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/doc/dirtree/$externalId/$dirId/$docId.tsx (95%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/doc/doc/$externalId.tsx (97%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/habit/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/journal/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/metric/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/metric/$externalId/$entryId.tsx (96%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/metric/entry/$externalId.tsx (97%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/person/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/schedule-event-full-days/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/schedule-event-in-day/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/schedule-stream/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/schedule-stream/$externalId/full-days-event/$eventId.tsx (96%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/schedule-stream/$externalId/in-day-event/$eventId.tsx (96%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/smart-list/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/smart-list/$externalId/$itemId.tsx (95%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/smart-list/item/$externalId.tsx (97%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/time-plan/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/todo-task/$externalId.tsx (98%) rename src/{webui/app/routes/app/public/published => published/app/routes/publish}/vacation/$externalId.tsx (97%) create mode 100644 src/published/package.json create mode 100644 src/published/package.mise.toml create mode 100644 src/published/remix.config.js create mode 100644 src/published/remix.env.d.ts create mode 100644 src/published/tsconfig.json create mode 100644 src/webui/app/logic/config.server.ts create mode 100644 src/webui/app/logic/config.ts diff --git a/.eslintignore b/.eslintignore index ff44cea67..d2a377986 100644 --- a/.eslintignore +++ b/.eslintignore @@ -19,4 +19,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -src/webui/remix.config.js \ No newline at end of file +src/webui/remix.config.js +src/published/remix.config.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 295696f07..1b775cbc8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -43,7 +43,11 @@ module.exports = { react: { version: 'detect' }, 'import/resolver': { typescript: { - project: ['src/webui/tsconfig.json', 'src/core/tsconfig.json'], + project: [ + 'src/webui/tsconfig.json', + 'src/published/tsconfig.json', + 'src/core/tsconfig.json', + ], }, }, }, @@ -57,6 +61,13 @@ module.exports = { project: ['src/webui/tsconfig.json'], }, }, + { + files: ['src/published/**/*.{ts,tsx}'], + parserOptions: { + tsconfigRootDir: __dirname, + project: ['src/published/tsconfig.json'], + }, + }, { files: ['src/core/**/*.{ts,tsx}'], parserOptions: { diff --git a/.gitignore b/.gitignore index 2151aa8a4..06c9566f5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ site .cache/ src/webui/build/ src/webui/public/build/ +src/published/build/ +src/published/public/build/ .env gen/ts/webapi-client/dist/ .sqlite diff --git a/AGENTS.md b/AGENTS.md index 5287ec7d9..fcf97355c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ Thrive (codenamed Jupiter) is a life planning tool with a monorepo containing: |---|---| | **WebAPI** (backend) | Python/FastAPI, SQLite | | **WebUI** (frontend) | TypeScript/Remix/React | +| **Published** (public pages) | TypeScript/Remix/React | | **API** | Python/FastAPI | | **MCP** | Python/FastAPI | | **Docs** | Python/MkDocs | @@ -27,7 +28,7 @@ mise run prepare ### Running services -Start all 5 services (WebAPI, API, MCP, WebUI, Docs) via mise: +Start all 6 services (WebAPI, API, MCP, WebUI, Published, Docs) via mise: ```bash mise run run:srv --instance <instance-name> diff --git a/docs/adrs/0010.entity-publish-mechanism.md b/docs/adrs/0010.entity-publish-mechanism.md index dacb4061e..ffc871b8f 100644 --- a/docs/adrs/0010.entity-publish-mechanism.md +++ b/docs/adrs/0010.entity-publish-mechanism.md @@ -44,13 +44,13 @@ added there explicitly; creation rejects unknown owner links. The canonical share link shown in the UI is: ```text -{webUiUrl}/app/public/published/{external_id} +{publishedUrl}/publish/{external_id} ``` -The route `src/webui/app/routes/app/public/published/$externalId.tsx` loads the +The route `src/published/app/routes/publish/$externalId.tsx` loads the publish entity as a guest, inspects `owner.the_type`, and **redirects** to a -typed public route (for example `/app/public/published/person/{externalId}` or -`/app/public/published/doc/dirtree/{externalId}/{dirRefId}` for directories). +typed public route (for example `/publish/person/{externalId}` or +`/publish/doc/dirtree/{externalId}/{dirRefId}` for directories). This keeps one stable link for copy/share while allowing entity-specific public layouts underneath. @@ -78,7 +78,7 @@ Shared validation logic for composite trees lives in dedicated services where needed (for example `DirPublishedLoadService` for docs directories). Public loader errors are normalized in -`src/webui/app/rendering/published-loader.server.ts` so guests see 404 for +`src/published/app/rendering/published-loader.server.ts` so guests see 404 for missing, inactive, or invalid publish links. ### Workspace UI: managing publish state @@ -108,7 +108,7 @@ inside entity load services when `include_publish_entity=True`). ### Public UI routes Each allowed owner type has one or more Remix routes under -`src/webui/app/routes/app/public/published/`. Public views: +`src/published/app/routes/publish/`. Public views: - Use read-only editors or listing components (often the same core components as workspace, with `inputsEnabled={false}`). @@ -139,7 +139,7 @@ the published tree. ### Docs directory routing note Published directory browsing uses a dedicated `dirtree/` URL branch -(`/app/public/published/doc/dirtree/{externalId}/{dirId}`) rather than nesting +(`/publish/doc/dirtree/{externalId}/{dirId}`) rather than nesting public dir routes under a parent that re-runs redirect loaders on every child navigation. `/doc/dir/{externalId}` exists only as a redirect into `dirtree/`. diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/app_core.py b/gen/py/webapi-client/jupiter_webapi_client/models/app_core.py index 96f4b05bd..58d680a18 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/app_core.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/app_core.py @@ -5,6 +5,7 @@ class AppCore(str, Enum): API = "api" CLI = "cli" MCP = "mcp" + PUBLISHED = "published" WEBUI = "webui" def __str__(self) -> str: diff --git a/gen/py/webapi-client/jupiter_webapi_client/models/working_mem_load_current_entry.py b/gen/py/webapi-client/jupiter_webapi_client/models/working_mem_load_current_entry.py index eff10f9af..79df75d48 100644 --- a/gen/py/webapi-client/jupiter_webapi_client/models/working_mem_load_current_entry.py +++ b/gen/py/webapi-client/jupiter_webapi_client/models/working_mem_load_current_entry.py @@ -1,13 +1,16 @@ from __future__ import annotations from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar, cast from attrs import define as _attrs_define from attrs import field as _attrs_field +from ..types import UNSET, Unset + if TYPE_CHECKING: from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.working_mem import WorkingMem @@ -21,17 +24,29 @@ class WorkingMemLoadCurrentEntry: Attributes: working_mem (WorkingMem): An entry in the working_mem.txt system. note (Note): A note in the notebook. + publish_entity (None | PublishEntity | Unset): """ working_mem: WorkingMem note: Note + publish_entity: None | PublishEntity | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.publish_entity import PublishEntity + working_mem = self.working_mem.to_dict() note = self.note.to_dict() + publish_entity: dict[str, Any] | None | Unset + if isinstance(self.publish_entity, Unset): + publish_entity = UNSET + elif isinstance(self.publish_entity, PublishEntity): + publish_entity = self.publish_entity.to_dict() + else: + publish_entity = self.publish_entity + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -40,12 +55,15 @@ def to_dict(self) -> dict[str, Any]: "note": note, } ) + if publish_entity is not UNSET: + field_dict["publish_entity"] = publish_entity return field_dict @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: from ..models.note import Note + from ..models.publish_entity import PublishEntity from ..models.working_mem import WorkingMem d = dict(src_dict) @@ -53,9 +71,27 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: note = Note.from_dict(d.pop("note")) + def _parse_publish_entity(data: object) -> None | PublishEntity | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + publish_entity_type_0 = PublishEntity.from_dict(data) + + return publish_entity_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | PublishEntity | Unset, data) + + publish_entity = _parse_publish_entity(d.pop("publish_entity", UNSET)) + working_mem_load_current_entry = cls( working_mem=working_mem, note=note, + publish_entity=publish_entity, ) working_mem_load_current_entry.additional_properties = d diff --git a/gen/ts/webapi-client/gen/models/AppCore.ts b/gen/ts/webapi-client/gen/models/AppCore.ts index 4bfa0cf32..b4714743e 100644 --- a/gen/ts/webapi-client/gen/models/AppCore.ts +++ b/gen/ts/webapi-client/gen/models/AppCore.ts @@ -8,6 +8,7 @@ export enum AppCore { CLI = 'cli', WEBUI = 'webui', + PUBLISHED = 'published', API = 'api', MCP = 'mcp', } diff --git a/gen/ts/webapi-client/gen/models/WorkingMemLoadCurrentEntry.ts b/gen/ts/webapi-client/gen/models/WorkingMemLoadCurrentEntry.ts index 9c91d0c18..d8b3d1303 100644 --- a/gen/ts/webapi-client/gen/models/WorkingMemLoadCurrentEntry.ts +++ b/gen/ts/webapi-client/gen/models/WorkingMemLoadCurrentEntry.ts @@ -3,6 +3,7 @@ /* tslint:disable */ /* eslint-disable */ import type { Note } from './Note'; +import type { PublishEntity } from './PublishEntity'; import type { WorkingMem } from './WorkingMem'; /** * Working mem load current entry. @@ -10,5 +11,6 @@ import type { WorkingMem } from './WorkingMem'; export type WorkingMemLoadCurrentEntry = { working_mem: WorkingMem; note: Note; + publish_entity?: (PublishEntity | null); }; diff --git a/infra/Config.infra b/infra/Config.infra index b6e4849b2..302da6100 100644 --- a/infra/Config.infra +++ b/infra/Config.infra @@ -1,5 +1,6 @@ GCP_LOGIN_FILE=../secrets/gcp-login.json WEBAPI_TESTING_PORT=8000 +PUBLISHED_TESTING_PORT=8001 SENTRY_AUTH_TOKEN=FAKEFAKE ALGOLIA_APP_ID=FAKEFAKE ALGOLIA_ADMIN_API_KEY=FAKEFAKE diff --git a/infra/self-hosted/compose.yaml b/infra/self-hosted/compose.yaml index f3d0f2089..4848aa50d 100644 --- a/infra/self-hosted/compose.yaml +++ b/infra/self-hosted/compose.yaml @@ -180,6 +180,31 @@ services: interval: 30s timeout: 10s retries: 5 + published: + environment: + - UNIVERSE=${UNIVERSE:?Check https://docs.get-thriving.com/how-tos/self-hosting} + - ENV=${ENV:-production} + - INSTANCE=${INSTANCE:?Check https://docs.get-thriving.com/how-tos/self-hosting} + - AUTH_PROVIDER=${AUTH_PROVIDER:-local} + - EMAIL_VERIFICATION_STRATEGY=${EMAIL_VERIFICATION_STRATEGY:-none} + - TELEMETRY=${TELEMETRY:-local} + - CRM=${CRM:-noop} + - PORT=2000 + - WEBAPI_SERVER_HOST=webapi + - WEBAPI_SERVER_PORT=2000 + - PUBLISHED_URL=${PUBLISHED_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}/publish} + - DOCS_URL=${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}/docs/ + restart: always + image: ${DOCKER_IMAGE_PUBLISHED:-getthriving/published:${VERSION:-latest}} + ports: + - "${PUBLISHED_PORT:-0}:2000" + depends_on: + - webapi + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:2000/healthz"] + interval: 30s + timeout: 10s + retries: 5 webui: environment: - UNIVERSE=${UNIVERSE:?Check https://docs.get-thriving.com/how-tos/self-hosting} @@ -197,6 +222,7 @@ services: - API_URL=${API_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}}/api - MCP_URL=${MCP_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}}/mcp - WEBUI_URL=${WEBUI_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}} + - PUBLISHED_URL=${PUBLISHED_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}/publish} - DOCS_URL=${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}/docs/ - SESSION_COOKIE_SECURE=${SESSION_COOKIE_SECURE:-false} - SESSION_COOKIE_SECRET=${SESSION_COOKIE_SECRET:?Check https://docs.get-thriving.com/how-tos/self-hosting} @@ -236,6 +262,7 @@ services: - "${WEBUI_PORT:-0}:443" depends_on: - webui + - published - api - mcp - webapi diff --git a/infra/self-hosted/webui.conf b/infra/self-hosted/webui.conf index 9929edaed..b1271f987 100644 --- a/infra/self-hosted/webui.conf +++ b/infra/self-hosted/webui.conf @@ -14,6 +14,18 @@ server { ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; + location /publish { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_read_timeout 5m; + proxy_buffering off; + + proxy_pass http://published:2000; + } + location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/infra/self-hosted/webui.nodomain.conf b/infra/self-hosted/webui.nodomain.conf index 26f239b3c..2971e8d9d 100644 --- a/infra/self-hosted/webui.nodomain.conf +++ b/infra/self-hosted/webui.nodomain.conf @@ -1,6 +1,18 @@ server { listen 80; + location /publish { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_read_timeout 5m; + proxy_buffering off; + + proxy_pass http://published:2000; + } + location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/infra/terraform.tf b/infra/terraform.tf index 7402c01a9..c5aabe1cb 100644 --- a/infra/terraform.tf +++ b/infra/terraform.tf @@ -284,6 +284,15 @@ resource "google_dns_record_set" "thrive_main_app_cname" { rrdatas = ["jupiter-webui.onrender.com."] } +resource "google_dns_record_set" "thrive_main_published_cname" { + project = google_project.get_thriving_main.project_id + managed_zone = google_dns_managed_zone.thrive_main.name + name = "published.get-thriving.com." + type = "CNAME" + ttl = 1800 + rrdatas = ["jupiter-published.onrender.com."] +} + resource "google_dns_record_set" "thrive_main_docs_cname" { project = google_project.get_thriving_main.project_id managed_zone = google_dns_managed_zone.thrive_main.name @@ -466,6 +475,12 @@ variable "WEBAPI_TESTING_PORT" { sensitive = false } +variable "PUBLISHED_TESTING_PORT" { + description = "On staging machines this http port is accessible for testing the published service" + type = string + sensitive = false +} + resource "google_compute_firewall" "default_allow_http" { project = google_project.get_thriving_main.project_id name = "default-allow-http" @@ -478,7 +493,7 @@ resource "google_compute_firewall" "default_allow_http" { allow { protocol = "tcp" - ports = ["80", var.WEBAPI_TESTING_PORT] + ports = ["80", var.WEBAPI_TESTING_PORT, var.PUBLISHED_TESTING_PORT] } } @@ -692,6 +707,12 @@ resource "docker_hub_repository" "webui" { description = "This is the repository for the WebUI" } +resource "docker_hub_repository" "published" { + namespace = var.DOCKER_REGISTRY_NAME + name = "published" + description = "This is the repository for the published pages service" +} + resource "docker_hub_repository" "docs" { namespace = var.DOCKER_REGISTRY_NAME name = "docs" @@ -870,6 +891,14 @@ resource "sentry_project" "webui" { platform = "javascript-remix" } +resource "sentry_project" "published" { + organization = data.sentry_organization.main.slug + teams = [sentry_team.thrive.slug] + name = "published" + slug = "published" + platform = "javascript-remix" +} + resource "sentry_project" "cli" { organization = data.sentry_organization.main.slug teams = [sentry_team.thrive.slug] diff --git a/itests/webui/entities/big_plans.test.py b/itests/webui/entities/big_plans.test.py index 4d6e15610..2a4fbfb47 100644 --- a/itests/webui/entities/big_plans.test.py +++ b/itests/webui/entities/big_plans.test.py @@ -129,10 +129,10 @@ def test_webui_big_plan_publish_and_view_public(page: Page, create_big_plan) -> expect(page.locator("#BigPlan-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/big-plan/")) + page.wait_for_url(re.compile(r"/publish/big-plan/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Published Big Plan") diff --git a/itests/webui/entities/chores.test.py b/itests/webui/entities/chores.test.py index 1fb2b50bd..9c0658e97 100644 --- a/itests/webui/entities/chores.test.py +++ b/itests/webui/entities/chores.test.py @@ -126,10 +126,10 @@ def test_webui_chore_publish_and_view_public(page: Page, create_chore) -> None: expect(page.locator("#Chore-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/chore/")) + page.wait_for_url(re.compile(r"/publish/chore/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Published Chore") diff --git a/itests/webui/entities/docs.test.py b/itests/webui/entities/docs.test.py index 0114a86db..64675165b 100644 --- a/itests/webui/entities/docs.test.py +++ b/itests/webui/entities/docs.test.py @@ -366,10 +366,10 @@ def test_webui_doc_publish_and_view_public(page: Page, create_doc) -> None: expect(page.locator("#Doc-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/doc/doc/")) + page.wait_for_url(re.compile(r"/publish/doc/doc/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Published Doc") @@ -398,21 +398,17 @@ def test_webui_dir_publish_and_view_public(page: Page, create_dir, create_doc) - expect(page.locator("#Dir-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url( - re.compile(rf"/app/public/published/doc/dirtree/[^/]+/{folder.ref_id}$") - ) + page.wait_for_url(re.compile(rf"/publish/doc/dirtree/[^/]+/{folder.ref_id}$")) page.wait_for_selector("#leaf-panel") expect(page.locator(f"#doc-{doc.ref_id}")).to_contain_text("Nested Published Doc") page.locator(f"#doc-{doc.ref_id} a").click() page.wait_for_url( - re.compile( - rf"/app/public/published/doc/dirtree/[^/]+/{folder.ref_id}/{doc.ref_id}" - ) + re.compile(rf"/publish/doc/dirtree/[^/]+/{folder.ref_id}/{doc.ref_id}") ) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Nested Published Doc") diff --git a/itests/webui/entities/habits.test.py b/itests/webui/entities/habits.test.py index c243f23e6..4537bb33e 100644 --- a/itests/webui/entities/habits.test.py +++ b/itests/webui/entities/habits.test.py @@ -126,10 +126,10 @@ def test_webui_habit_publish_and_view_public(page: Page, create_habit) -> None: expect(page.locator("#Habit-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/habit/")) + page.wait_for_url(re.compile(r"/publish/habit/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Published Habit") diff --git a/itests/webui/entities/journals.test.py b/itests/webui/entities/journals.test.py index 9f0d5a2d7..b3b339b27 100644 --- a/itests/webui/entities/journals.test.py +++ b/itests/webui/entities/journals.test.py @@ -122,10 +122,10 @@ def test_webui_journal_publish_and_view_public(page: Page, create_journal) -> No expect(page.locator("#Journal-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/journal/")) + page.wait_for_url(re.compile(r"/publish/journal/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="rightNow"]')).to_have_value("2024-07-01") diff --git a/itests/webui/entities/metrics.test.py b/itests/webui/entities/metrics.test.py index 9730591b3..5263af7f5 100644 --- a/itests/webui/entities/metrics.test.py +++ b/itests/webui/entities/metrics.test.py @@ -181,10 +181,10 @@ def test_webui_metric_entry_publish_and_view_public( expect(page.locator("#MetricEntry-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/metric/entry/")) + page.wait_for_url(re.compile(r"/publish/metric/entry/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="collectionTime"]')).to_have_value("2024-01-15") @@ -215,10 +215,10 @@ def test_webui_metric_publish_and_view_public( expect(page.locator("#Metric-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/metric/")) + page.wait_for_url(re.compile(r"/publish/metric/")) page.wait_for_selector("#leaf-panel") expect(page.locator(f"#metric-entry-{entry.ref_id}")).to_contain_text("25") @@ -244,10 +244,10 @@ def test_webui_metric_entry_view_public( public_url = page.locator('input[name="publicUrl"]').input_value() page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/metric/")) + page.wait_for_url(re.compile(r"/publish/metric/")) page.locator(f"#metric-entry-{entry.ref_id}").click() - page.wait_for_url(re.compile(rf"/app/public/published/metric/[^/]+/{entry.ref_id}")) + page.wait_for_url(re.compile(rf"/publish/metric/[^/]+/{entry.ref_id}")) page.wait_for_selector("#leaflet-panel") expect(page.locator('input[name="collectionTime"]')).to_have_value("2024-02-01") diff --git a/itests/webui/entities/persons.test.py b/itests/webui/entities/persons.test.py index b84b49366..9e52490ae 100644 --- a/itests/webui/entities/persons.test.py +++ b/itests/webui/entities/persons.test.py @@ -88,10 +88,10 @@ def test_webui_person_publish_and_view_public(page: Page, create_person) -> None expect(page.locator("#Person-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/person/")) + page.wait_for_url(re.compile(r"/publish/person/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Published Person") diff --git a/itests/webui/entities/schedules.test.py b/itests/webui/entities/schedules.test.py index 76ece1f3b..92c6a04b4 100644 --- a/itests/webui/entities/schedules.test.py +++ b/itests/webui/entities/schedules.test.py @@ -226,10 +226,10 @@ def test_webui_schedule_event_in_day_publish_and_view_public( expect(page.locator("#ScheduleEventInDay-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/schedule-event-in-day/")) + page.wait_for_url(re.compile(r"/publish/schedule-event-in-day/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Published In Day Event") @@ -267,10 +267,10 @@ def test_webui_schedule_stream_publish_and_view_public( page.wait_for_selector("#leaf-panel") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/schedule-stream/")) + page.wait_for_url(re.compile(r"/publish/schedule-stream/")) page.wait_for_selector("#leaf-panel") page.goto(f"{public_url.split('?')[0]}?date=2024-07-01&period=daily&view=calendar") @@ -317,10 +317,10 @@ def test_webui_schedule_event_full_days_publish_and_view_public( expect(page.locator("#ScheduleEventFullDays-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/schedule-event-full-days/")) + page.wait_for_url(re.compile(r"/publish/schedule-event-full-days/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value( diff --git a/itests/webui/entities/smart_list.test.py b/itests/webui/entities/smart_list.test.py index 45cc14bf6..0f1ce8b1d 100644 --- a/itests/webui/entities/smart_list.test.py +++ b/itests/webui/entities/smart_list.test.py @@ -163,10 +163,10 @@ def test_webui_smart_list_publish_and_view_public( expect(page.locator("#SmartList-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/smart-list/")) + page.wait_for_url(re.compile(r"/publish/smart-list/")) page.wait_for_selector("#branch-panel") expect(page.locator(f"#smart-list-item-{item.ref_id}")).to_contain_text( @@ -194,12 +194,10 @@ def test_webui_smart_list_item_view_public( public_url = page.locator('input[name="publicUrl"]').input_value() page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/smart-list/")) + page.wait_for_url(re.compile(r"/publish/smart-list/")) page.locator(f"#smart-list-item-{item.ref_id}").click() - page.wait_for_url( - re.compile(rf"/app/public/published/smart-list/[^/]+/{item.ref_id}") - ) + page.wait_for_url(re.compile(rf"/publish/smart-list/[^/]+/{item.ref_id}")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Public Item Detail") @@ -233,10 +231,10 @@ def test_webui_smart_list_item_publish_and_view_public( expect(page.locator("#SmartListItem-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/smart-list/item/")) + page.wait_for_url(re.compile(r"/publish/smart-list/item/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value( diff --git a/itests/webui/entities/time_plans.test.py b/itests/webui/entities/time_plans.test.py index 392f29edd..c61caa528 100644 --- a/itests/webui/entities/time_plans.test.py +++ b/itests/webui/entities/time_plans.test.py @@ -341,10 +341,10 @@ def test_webui_time_plan_publish_and_view_public(page: Page, create_time_plan) - expect(page.locator("#TimePlan-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/time-plan/")) + page.wait_for_url(re.compile(r"/publish/time-plan/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="rightNow"]')).to_have_value("2024-06-18") diff --git a/itests/webui/entities/todos.test.py b/itests/webui/entities/todos.test.py index 5f244e6bc..88cb33e89 100644 --- a/itests/webui/entities/todos.test.py +++ b/itests/webui/entities/todos.test.py @@ -197,10 +197,10 @@ def test_webui_todo_publish_and_view_public(page: Page, create_todo) -> None: expect(page.locator("#TodoTask-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/todo-task/")) + page.wait_for_url(re.compile(r"/publish/todo-task/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Published Todo") diff --git a/itests/webui/entities/vacations.test.py b/itests/webui/entities/vacations.test.py index ad3469ff2..0d61f06f7 100644 --- a/itests/webui/entities/vacations.test.py +++ b/itests/webui/entities/vacations.test.py @@ -213,10 +213,10 @@ def test_webui_vacation_publish_and_view_public(page: Page, create_vacation) -> expect(page.locator("#Vacation-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() - assert "/app/public/published/" in public_url + assert "/publish/" in public_url page.goto(public_url) - page.wait_for_url(re.compile(r"/app/public/published/vacation/")) + page.wait_for_url(re.compile(r"/publish/vacation/")) page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Published Vacation") diff --git a/mise.toml b/mise.toml index 0bf07af85..b04f3957b 100644 --- a/mise.toml +++ b/mise.toml @@ -49,6 +49,7 @@ includes = [ "src/mcp/package.mise.toml", "src/docs/package.mise.toml", "src/webui/package.mise.toml", + "src/published/package.mise.toml", "src/desktop/package.mise.toml", "src/mobile/package.mise.toml", "itests/package.mise.toml", diff --git a/mise.toml.hbs b/mise.toml.hbs index 1927b234e..63660d9bd 100644 --- a/mise.toml.hbs +++ b/mise.toml.hbs @@ -49,6 +49,7 @@ includes = [ "src/mcp/package.mise.toml", "src/docs/package.mise.toml", "src/webui/package.mise.toml", + "src/published/package.mise.toml", "src/desktop/package.mise.toml", "src/mobile/package.mise.toml", "itests/package.mise.toml", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84241ca00..edf65aaca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,6 +302,124 @@ importers: specifier: ^1.5.0 version: 1.6.0 + src/published: + dependencies: + '@calumk/editorjs-codecup': + specifier: ^1.3.0 + version: 1.3.0 + '@editorjs/checklist': + specifier: ^1.6.0 + version: 1.6.0 + '@editorjs/delimiter': + specifier: ^1.4.2 + version: 1.4.2 + '@editorjs/editorjs': + specifier: ^2.31.1 + version: 2.31.1 + '@editorjs/header': + specifier: ^2.8.8 + version: 2.8.8 + '@editorjs/nested-list': + specifier: ^1.4.3 + version: 1.4.3 + '@editorjs/quote': + specifier: ^2.7.6 + version: 2.7.6 + '@editorjs/table': + specifier: ^2.4.4 + version: 2.4.5 + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@18.3.26)(react@18.3.1) + '@emotion/styled': + specifier: ^11.14.0 + version: 11.14.1(@emotion/react@11.14.0(@types/react@18.3.26)(react@18.3.1))(@types/react@18.3.26)(react@18.3.1) + '@jupiter/core': + specifier: workspace:* + version: link:../core + '@jupiter/webapi-client': + specifier: workspace:* + version: link:../../gen/ts/webapi-client + '@mui/icons-material': + specifier: ^6.4.8 + version: 6.5.0(@mui/material@6.5.0(@emotion/react@11.14.0(@types/react@18.3.26)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.26)(react@18.3.1))(@types/react@18.3.26)(react@18.3.1))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.26)(react@18.3.1) + '@mui/material': + specifier: ^6.4.8 + version: 6.5.0(@emotion/react@11.14.0(@types/react@18.3.26)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.26)(react@18.3.1))(@types/react@18.3.26)(react@18.3.1))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/calendar': + specifier: '=0.80.0' + version: 0.80.0(@nivo/core@0.80.0)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/core': + specifier: '=0.80.0' + version: 0.80.0(@nivo/tooltip@0.80.0)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@nivo/line': + specifier: '=0.80.0' + version: 0.80.0(@nivo/core@0.80.0)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@remix-run/node': + specifier: ^2.16.3 + version: 2.17.2(typescript@5.9.3) + '@remix-run/react': + specifier: ^2.16.3 + version: 2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@remix-run/serve': + specifier: ^2.16.3 + version: 2.17.2(typescript@5.9.3) + buffer-polyfill: + specifier: npm:buffer@^6.0.3 + version: buffer@6.0.3 + dotenv: + specifier: ^16.6.1 + version: 16.6.1 + editorjs-drag-drop: + specifier: ^1.1.16 + version: 1.1.16 + framer-motion: + specifier: ^12.5.0 + version: 12.23.24(@emotion/is-prop-valid@1.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + http-status-codes: + specifier: ^2.3.0 + version: 2.3.0 + isbot: + specifier: ^5.1.25 + version: 5.1.32 + luxon: + specifier: ^3.5.0 + version: 3.7.2 + notistack: + specifier: ^3.0.2 + version: 3.0.2(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + zod: + specifier: ^3.24.2 + version: 3.25.76 + zodix: + specifier: ^0.4.4 + version: 0.4.4(@remix-run/server-runtime@2.17.2(typescript@5.9.3))(zod@3.25.76) + devDependencies: + '@remix-run/dev': + specifier: ^2.16.3 + version: 2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1) + '@remix-run/v1-route-convention': + specifier: ^0.1.4 + version: 0.1.4(@remix-run/dev@2.17.2(@remix-run/react@2.17.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@remix-run/serve@2.17.2(typescript@5.9.3))(@types/node@24.10.0)(babel-plugin-macros@3.1.0)(jiti@2.6.1)(terser@5.44.1)(ts-node@10.9.2(@types/node@24.10.0)(typescript@5.9.3))(typescript@5.9.3)(vite@6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1)) + '@types/luxon': + specifier: ^3.4.2 + version: 3.7.1 + '@types/react': + specifier: ^18.0.25 + version: 18.3.26 + '@types/react-dom': + specifier: ^18.0.8 + version: 18.3.7(@types/react@18.3.26) + vite: + specifier: ^6.4.1 + version: 6.4.1(@types/node@24.10.0)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) + src/webui: dependencies: '@calumk/editorjs-codecup': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d733898ec..2823d104e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - "gen/ts/webapi-client" - "src/core" - "src/webui" + - "src/published" - "src/desktop" - "src/mobile" diff --git a/render.yaml b/render.yaml index 28f60ff05..c2ef77781 100644 --- a/render.yaml +++ b/render.yaml @@ -129,6 +129,65 @@ services: - key: EMAIL_VERIFICATION_STRATEGY value: verify previewValue: none + - key: PUBLISHED_URL + value: https://published.get-thriving.com +- type: web + name: jupiter-published + buildFilter: + paths: + - src/published/** + - src/core/jupiter/core/** + - gen/** + - package.json + - pnpm-lock.yaml + - pnpm-workspace.yaml + - assets/** + - src/Config.global + runtime: docker + dockerfilePath: ./src/published/Dockerfile + dockerContext: . + region: frankfurt + plan: starter + autoDeploy: true + rootDir: . + numInstances: 1 + healthCheckPath: /healthz + domains: + - published.get-thriving.com + previews: + generation: automatic + plan: starter + envVars: + - key: UNIVERSE + value: thrive + - key: ENV + value: production + previewValue: staging + - key: INSTANCE + value: Main + previewValue: TO-FILL + - key: WEBAPI_SERVER_HOST + fromService: + name: jupiter-webapi-srv + type: web + property: host + - key: WEBAPI_SERVER_PORT + fromService: + name: jupiter-webapi-srv + type: web + property: port + - key: PUBLISHED_URL + fromService: + name: jupiter-published + type: web + envVarKey: RENDER_EXTERNAL_URL + - key: DOCS_URL + value: https://docs.get-thriving.com + - key: AUTH_PROVIDER + value: local-google-apple + - key: EMAIL_VERIFICATION_STRATEGY + value: verify + previewValue: none - type: web name: jupiter-webapi-srv buildFilter: diff --git a/src/Config.global b/src/Config.global index 4d1dc38ed..b08087e73 100644 --- a/src/Config.global +++ b/src/Config.global @@ -7,6 +7,7 @@ VERSION=1.3.3 BUNDLE_ID=io.jupiter.app GLOBAL_HOSTED_INFRA_ROOT=onrender.com HOSTED_GLOBAL_WEBUI_URL=https://app.get-thriving.com +HOSTED_GLOBAL_PUBLISHED_URL=https://published.get-thriving.com HOSTED_GLOBAL_WEBAPI_URL=https://webapi.infra.get-thriving.com HOSTED_GLOBAL_API_URL=https://api.get-thriving.com HOSTED_GLOBAL_MCP_URL=https://mcp.get-thriving.com diff --git a/src/core/jupiter/core/app.py b/src/core/jupiter/core/app.py index 4a8f23fe0..d2da2f451 100644 --- a/src/core/jupiter/core/app.py +++ b/src/core/jupiter/core/app.py @@ -84,6 +84,7 @@ class AppCore(EnumValue): CLI = "cli" WEBUI = "webui" + PUBLISHED = "published" API = "api" MCP = "mcp" diff --git a/src/core/jupiter/core/auth/sub/google/google-oauth-redirect-state.server.ts b/src/core/jupiter/core/auth/sub/google/google-oauth-redirect-state.server.ts index dd0cb48d1..2b5221ec0 100644 --- a/src/core/jupiter/core/auth/sub/google/google-oauth-redirect-state.server.ts +++ b/src/core/jupiter/core/auth/sub/google/google-oauth-redirect-state.server.ts @@ -1,4 +1,4 @@ -import { GLOBAL_PROPERTIES, SERVICE_PROPERTIES } from "#/core/config-server"; +import { GLOBAL_PROPERTIES } from "#/core/config-server"; import { isLocal } from "#/core/env"; export interface GoogleOauthRedirectState { @@ -22,7 +22,10 @@ function hostnameMatchesInfraRoot( return hostname === normalizedRoot || hostname.endsWith(`.${normalizedRoot}`); } -export function isAllowedGoogleOauthCallbackUrl(url: string): boolean { +export function isAllowedGoogleOauthCallbackUrl( + url: string, + localWebUiUrl: string, +): boolean { const hostname = hostnameForUrl(url); if (hostname === null) { return false; @@ -42,7 +45,7 @@ export function isAllowedGoogleOauthCallbackUrl(url: string): boolean { } if (isLocal(GLOBAL_PROPERTIES.env)) { - const localWebUiHostname = hostnameForUrl(SERVICE_PROPERTIES.webUiUrl); + const localWebUiHostname = hostnameForUrl(localWebUiUrl); if (localWebUiHostname !== null && hostname === localWebUiHostname) { return true; } diff --git a/src/core/jupiter/core/auth/sub/google/oauth-state.server.ts b/src/core/jupiter/core/auth/sub/google/oauth-state.server.ts index e09626cb2..0d6fd7f45 100644 --- a/src/core/jupiter/core/auth/sub/google/oauth-state.server.ts +++ b/src/core/jupiter/core/auth/sub/google/oauth-state.server.ts @@ -1,24 +1,27 @@ import { createCookie } from "@remix-run/node"; -import { SERVICE_PROPERTIES } from "#/core/config-server"; import { GOOGLE_OAUTH_STATE_COOKIE_NAME } from "#/core/infra/names"; -const googleOauthStateCookie = createCookie(GOOGLE_OAUTH_STATE_COOKIE_NAME, { - httpOnly: true, - maxAge: 60 * 10, - path: "/", - sameSite: "lax", - secure: SERVICE_PROPERTIES.sessionCookieSecure, -}); +// The cookie security setting is service-level configuration, so callers +// pass it in rather than core reaching into a service's config. +function googleOauthStateCookie(secure: boolean) { + return createCookie(GOOGLE_OAUTH_STATE_COOKIE_NAME, { + httpOnly: true, + maxAge: 60 * 10, + path: "/", + sameSite: "lax", + secure: secure, + }); +} -export async function saveGoogleOauthState(state: string) { - return await googleOauthStateCookie.serialize(state); +export async function saveGoogleOauthState(state: string, secure: boolean) { + return await googleOauthStateCookie(secure).serialize(state); } export async function loadGoogleOauthState(cookieHeader: string | null) { - return await googleOauthStateCookie.parse(cookieHeader); + return await googleOauthStateCookie(false).parse(cookieHeader); } -export async function clearGoogleOauthState() { - return await googleOauthStateCookie.serialize("", { maxAge: 0 }); +export async function clearGoogleOauthState(secure: boolean) { + return await googleOauthStateCookie(secure).serialize("", { maxAge: 0 }); } diff --git a/src/core/jupiter/core/big_plans/component/card.tsx b/src/core/jupiter/core/big_plans/component/card.tsx index 33fb66223..5e42e87d6 100644 --- a/src/core/jupiter/core/big_plans/component/card.tsx +++ b/src/core/jupiter/core/big_plans/component/card.tsx @@ -19,7 +19,7 @@ import { isWorkspaceFeatureAvailable } from "#/core/workspaces/root"; import { bigPlanDonePct, type BigPlanParent } from "#/core/big_plans/root"; import { isCompleted } from "#/core/big_plans/status"; import { ClientOnly } from "#/core/infra/component/client-only"; -import { ServicePropertiesContext } from "#/core/config-client"; +import { OverdueThresholdsContext } from "#/core/infra/overdue-thresholds-context"; import type { TopLevelInfo } from "#/core/infra/top-level-context"; import { ADateTag } from "#/core/common/component/adate-tag"; import { BigPlanStatusTag } from "#/core/big_plans/component/status-tag"; @@ -182,7 +182,7 @@ interface OverdueWarningProps { } function OverdueWarning({ today, status, dueDate }: OverdueWarningProps) { - const serviceProperties = useContext(ServicePropertiesContext); + const overdueThresholds = useContext(OverdueThresholdsContext); if (isCompleted(status)) { return null; @@ -200,17 +200,17 @@ function OverdueWarning({ today, status, dueDate }: OverdueWarningProps) { {() => { if ( theDueDate <= - theToday.minus({ days: serviceProperties.overdueDangerDays }) + theToday.minus({ days: overdueThresholds.overdueDangerDays }) ) { return <OverdueWarningChip label="Overdue" color="error" />; } else if ( theDueDate <= - theToday.minus({ days: serviceProperties.overdueWarningDays }) + theToday.minus({ days: overdueThresholds.overdueWarningDays }) ) { return <OverdueWarningChip label="Overdue" color="warning" />; } else if ( theDueDate <= - theToday.minus({ days: serviceProperties.overdueInfoDays }) + theToday.minus({ days: overdueThresholds.overdueInfoDays }) ) { return <OverdueWarningChip label="Overdue" color="info" />; } diff --git a/src/core/jupiter/core/calendar/component/calendar-navigation.tsx b/src/core/jupiter/core/calendar/component/calendar-navigation.tsx index bd4b0b619..6495c79a0 100644 --- a/src/core/jupiter/core/calendar/component/calendar-navigation.tsx +++ b/src/core/jupiter/core/calendar/component/calendar-navigation.tsx @@ -44,7 +44,7 @@ function workspaceCalendarNavigation(): CalendarNavigationValue { export function publishedScheduleStreamCalendarNavigation( externalId: string, ): CalendarNavigationValue { - const calendarBasePath = `/app/public/published/schedule-stream/${externalId}`; + const calendarBasePath = `/publish/schedule-stream/${externalId}`; return { eventPath: (kind, refId) => { switch (kind) { diff --git a/src/core/jupiter/core/common/sub/inbox_tasks/component/card.tsx b/src/core/jupiter/core/common/sub/inbox_tasks/component/card.tsx index f1279b647..1d8467098 100644 --- a/src/core/jupiter/core/common/sub/inbox_tasks/component/card.tsx +++ b/src/core/jupiter/core/common/sub/inbox_tasks/component/card.tsx @@ -38,7 +38,7 @@ import type { InboxTaskParent, } from "#/core/common/sub/inbox_tasks/root"; import { ClientOnly } from "#/core/infra/component/client-only"; -import { ServicePropertiesContext } from "#/core/config-client"; +import { OverdueThresholdsContext } from "#/core/infra/overdue-thresholds-context"; import { useBigScreen } from "#/core/infra/component/use-big-screen"; import type { TopLevelInfo } from "#/core/infra/top-level-context"; import { ADateTag } from "#/core/common/component/adate-tag"; @@ -387,7 +387,7 @@ interface OverdueWarningProps { } function OverdueWarning({ today, status, dueDate }: OverdueWarningProps) { - const serviceProperties = useContext(ServicePropertiesContext); + const overdueThresholds = useContext(OverdueThresholdsContext); if (isCompleted(status)) { return null; @@ -405,17 +405,17 @@ function OverdueWarning({ today, status, dueDate }: OverdueWarningProps) { {() => { if ( theDueDate <= - theToday.minus({ days: serviceProperties.overdueDangerDays }) + theToday.minus({ days: overdueThresholds.overdueDangerDays }) ) { return <OverdueWarningChip label="Overdue" color="error" />; } else if ( theDueDate <= - theToday.minus({ days: serviceProperties.overdueWarningDays }) + theToday.minus({ days: overdueThresholds.overdueWarningDays }) ) { return <OverdueWarningChip label="Overdue" color="warning" />; } else if ( theDueDate <= - theToday.minus({ days: serviceProperties.overdueInfoDays }) + theToday.minus({ days: overdueThresholds.overdueInfoDays }) ) { return <OverdueWarningChip label="Overdue" color="info" />; } diff --git a/src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx b/src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx index b30ef38c8..207e2b4e2 100644 --- a/src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx +++ b/src/core/jupiter/core/common/sub/publish/components/publish-panel.tsx @@ -16,7 +16,8 @@ import { import { useContext, useState } from "react"; import { entityLinkStd } from "#/core/common/entity-link"; -import { ServicePropertiesContext } from "#/core/config-client"; +import { publishedShareUrl } from "#/core/common/sub/publish/published-share-url"; +import { ServiceLinksContext } from "#/core/infra/service-links-context"; import { ActionSingle, SectionActions, @@ -33,13 +34,16 @@ interface PublishPanelProps { } export function PublishPanel(props: PublishPanelProps) { - const serviceProperties = useContext(ServicePropertiesContext); + const serviceLinks = useContext(ServiceLinksContext); const [hasCopiedPublicUrl, setHasCopiedPublicUrl] = useState(false); const sectionId = `${props.entityType}-publish`; const publishOwner = entityLinkStd(props.entityType, props.entityRefId); const publicUrl = props.publishEntity !== null - ? `${serviceProperties.webUiUrl}/app/public/published/${props.publishEntity.external_id}` + ? publishedShareUrl( + serviceLinks.publishedUrl, + props.publishEntity.external_id, + ) : ""; const isActive = props.publishEntity?.status === PublishEntityStatus.ACTIVE; diff --git a/src/core/jupiter/core/common/sub/publish/published-share-url.ts b/src/core/jupiter/core/common/sub/publish/published-share-url.ts new file mode 100644 index 000000000..7b992f4f9 --- /dev/null +++ b/src/core/jupiter/core/common/sub/publish/published-share-url.ts @@ -0,0 +1,11 @@ +export const PUBLISHED_ROUTE_PREFIX = "/publish"; + +export function publishedShareUrl( + publishedUrl: string, + externalId: string, +): string { + const base = publishedUrl.endsWith("/") + ? publishedUrl.slice(0, -1) + : publishedUrl; + return `${base}${PUBLISHED_ROUTE_PREFIX}/${externalId}`; +} diff --git a/src/core/jupiter/core/config-client.ts b/src/core/jupiter/core/config-client.ts index f36fa5235..c140290ea 100644 --- a/src/core/jupiter/core/config-client.ts +++ b/src/core/jupiter/core/config-client.ts @@ -1,8 +1,4 @@ import { - AppCore, - AppDistribution, - AppPlatform, - AppShell, Env, Instance, JupiterAuthProvider, @@ -12,11 +8,7 @@ import { } from "@jupiter/webapi-client"; import { createContext } from "react"; -import type { - GlobalPropertiesServer, - ServicePropertiesServer, -} from "#/core/config-server"; -import type { FrontDoorInfo } from "#/core/frontdoor"; +import type { GlobalPropertiesServer } from "#/core/config-server"; export interface GlobalPropertiesClient { publicName: string; @@ -34,21 +26,6 @@ export interface GlobalPropertiesClient { privacyPolicyUrl: string; } -export interface ServicePropertiesClient { - appCore: AppCore; - frontDoorInfo: FrontDoorInfo; - webApiProgressReporterUrl: string; - webApiUrl: string; - apiUrl: string; - mcpUrl: string; - webUiUrl: string; - docsUrl: string; - inboxTasksToAskForGC: number; - overdueInfoDays: number; - overdueWarningDays: number; - overdueDangerDays: number; -} - export const GlobalPropertiesContext = createContext<GlobalPropertiesClient>({ publicName: "FAKE-FAKE", description: "FAKE-FAKE", @@ -65,27 +42,6 @@ export const GlobalPropertiesContext = createContext<GlobalPropertiesClient>({ privacyPolicyUrl: "FAKE-FAKE", }); -export const ServicePropertiesContext = createContext<ServicePropertiesClient>({ - appCore: AppCore.WEBUI, - frontDoorInfo: { - clientVersion: "FAKE-FAKE", - appShell: AppShell.BROWSER, - appPlatform: AppPlatform.DESKTOP_MACOS, - appDistribution: AppDistribution.WEB, - initialWindowWidth: undefined, - }, - webApiProgressReporterUrl: "FAKE-FAKE", - webApiUrl: "FAKE-FAKE", - apiUrl: "FAKE-FAKE", - mcpUrl: "FAKE-FAKE", - webUiUrl: "FAKE-FAKE", - docsUrl: "FAKE-FAKE", - inboxTasksToAskForGC: 20, - overdueInfoDays: 1, - overdueWarningDays: 2, - overdueDangerDays: 3, -}); - export function serverToClientGlobalProperties( globalPropertiesServer: GlobalPropertiesServer, ): GlobalPropertiesClient { @@ -105,24 +61,3 @@ export function serverToClientGlobalProperties( privacyPolicyUrl: globalPropertiesServer.privacyPolicyUrl, }; } - -export function serverToClientServiceProperties( - servicePropertiesServer: ServicePropertiesServer, - frontDoorInfo: FrontDoorInfo, -): ServicePropertiesClient { - return { - appCore: AppCore.WEBUI, - frontDoorInfo: frontDoorInfo, - webApiProgressReporterUrl: - servicePropertiesServer.webApiProgressReporterUrl, - webApiUrl: servicePropertiesServer.webApiUrl, - apiUrl: servicePropertiesServer.apiUrl, - mcpUrl: servicePropertiesServer.mcpUrl, - webUiUrl: servicePropertiesServer.webUiUrl, - docsUrl: servicePropertiesServer.docsUrl, - inboxTasksToAskForGC: servicePropertiesServer.inboxTasksToAskForGC, - overdueInfoDays: servicePropertiesServer.overdueInfoDays, - overdueWarningDays: servicePropertiesServer.overdueWarningDays, - overdueDangerDays: servicePropertiesServer.overdueDangerDays, - }; -} diff --git a/src/core/jupiter/core/config-server.ts b/src/core/jupiter/core/config-server.ts index 15e0f1ce2..c185a566d 100644 --- a/src/core/jupiter/core/config-server.ts +++ b/src/core/jupiter/core/config-server.ts @@ -24,6 +24,7 @@ export interface GlobalPropertiesServer { telemetry: JupiterTelemetry; crmBackend: JupiterCrmBackend; hostedGlobalWebUiUrl: string; + hostedGlobalPublishedUrl: string; globalHostedInfraRoot: string; communityUrl: string; appsStorageUrl: string; @@ -34,23 +35,6 @@ export interface GlobalPropertiesServer { privacyPolicyUrl: string; } -export interface ServicePropertiesServer { - webApiServerUrl: string; - webApiProgressReporterUrl: string; - webApiUrl: string; - apiUrl: string; - mcpUrl: string; - webUiUrl: string; - docsUrl: string; - pwaStartUrl: string; - sessionCookieSecure: boolean; - sessionCookieSecret: string; - inboxTasksToAskForGC: number; - overdueInfoDays: number; - overdueWarningDays: number; - overdueDangerDays: number; -} - // @secureFn function loadGlobalPropertiesOnServer(): GlobalPropertiesServer { config({ path: `${process.cwd()}/../Config.global` }); @@ -73,6 +57,7 @@ function loadGlobalPropertiesOnServer(): GlobalPropertiesServer { telemetry: (process.env.TELEMETRY ?? "local") as JupiterTelemetry, crmBackend: (process.env.CRM ?? "noop") as JupiterCrmBackend, hostedGlobalWebUiUrl: process.env.HOSTED_GLOBAL_WEBUI_URL as string, + hostedGlobalPublishedUrl: process.env.HOSTED_GLOBAL_PUBLISHED_URL as string, globalHostedInfraRoot: process.env.GLOBAL_HOSTED_INFRA_ROOT as string, communityUrl: process.env.COMMUNITY_URL as string, appsStorageUrl: process.env.APPS_STORAGE_URL as string, @@ -86,7 +71,9 @@ function loadGlobalPropertiesOnServer(): GlobalPropertiesServer { return globalProperties; } -function resolveWebUiUrl(globalProperties: GlobalPropertiesServer): string { +export function resolveWebUiUrl( + globalProperties: GlobalPropertiesServer, +): string { const webUiUrl = process.env.WEBUI_URL as string; if ( @@ -99,64 +86,39 @@ function resolveWebUiUrl(globalProperties: GlobalPropertiesServer): string { return webUiUrl; } -function loadServicePropertiesOnServer( +export function resolvePublishedUrl( globalProperties: GlobalPropertiesServer, -): ServicePropertiesServer { - config({ path: `${process.cwd()}/Config.project` }); +): string { + const publishedUrl = process.env.PUBLISHED_URL as string; - const webApiServerHost = process.env.WEBAPI_SERVER_HOST as string; - const webApiServerPort = parseInt( - process.env.WEBAPI_SERVER_PORT as string, - 10, - ); - - const webApiServerUrl = `http://${webApiServerHost}:${webApiServerPort}`; - const webApiProgressReporterUrl = process.env - .WEBAPI_PROGRESS_REPORTER_URL as string; - - const serviceProperties = { - webApiServerUrl: webApiServerUrl, - webApiProgressReporterUrl: webApiProgressReporterUrl, - webApiUrl: process.env.WEBAPI_URL as string, - apiUrl: process.env.API_URL as string, - mcpUrl: process.env.MCP_URL as string, - webUiUrl: resolveWebUiUrl(globalProperties), - docsUrl: process.env.DOCS_URL as string, - pwaStartUrl: process.env.PWA_START_URL as string, - sessionCookieSecure: process.env.SESSION_COOKIE_SECURE === "true", - sessionCookieSecret: process.env.SESSION_COOKIE_SECRET as string, - inboxTasksToAskForGC: parseInt( - process.env.INBOX_TASKS_TO_ASK_FOR_GC as string, - 10, - ), - overdueInfoDays: parseInt(process.env.OVERDUE_INFO_DAYS as string, 10), - overdueWarningDays: parseInt( - process.env.OVERDUE_WARNING_DAYS as string, - 10, - ), - overdueDangerDays: parseInt(process.env.OVERDUE_DANGER_DAYS as string, 10), - }; + if ( + isThriveUniverse(globalProperties.universe) && + globalProperties.env === Env.PRODUCTION + ) { + return globalProperties.hostedGlobalPublishedUrl; + } - return serviceProperties; + return publishedUrl; } export const GLOBAL_PROPERTIES = loadGlobalPropertiesOnServer(); -export const SERVICE_PROPERTIES = - loadServicePropertiesOnServer(GLOBAL_PROPERTIES); -// A hack! -console.log("=".repeat(80)); -console.log(`Starting Jupiter WebUI:`); -console.log(` Version: ${GLOBAL_PROPERTIES.version}`); -console.log(` Universe: ${GLOBAL_PROPERTIES.universe}`); -console.log(` Environment: ${GLOBAL_PROPERTIES.env}`); -console.log(` Instance: ${GLOBAL_PROPERTIES.instance}`); -console.log(` Hosting: ${getHosting(GLOBAL_PROPERTIES.universe)}`); -console.log("-".repeat(80)); -console.log(` Auth Provider: ${GLOBAL_PROPERTIES.authProvider}`); -console.log( - ` Email Verification Strategy: ${GLOBAL_PROPERTIES.emailVerificationStrategy}`, -); -console.log(` CRM Backend: ${GLOBAL_PROPERTIES.crmBackend}`); -console.log(` Telemetry: ${GLOBAL_PROPERTIES.telemetry}`); -console.log("=".repeat(80)); +// Each service calls this once from its own config module, mirroring the +// startup banners that the Python services print. +export function logServiceStartupBanner(serviceName: string) { + console.log("=".repeat(80)); + console.log(`Starting Jupiter ${serviceName}:`); + console.log(` Version: ${GLOBAL_PROPERTIES.version}`); + console.log(` Universe: ${GLOBAL_PROPERTIES.universe}`); + console.log(` Environment: ${GLOBAL_PROPERTIES.env}`); + console.log(` Instance: ${GLOBAL_PROPERTIES.instance}`); + console.log(` Hosting: ${getHosting(GLOBAL_PROPERTIES.universe)}`); + console.log("-".repeat(80)); + console.log(` Auth Provider: ${GLOBAL_PROPERTIES.authProvider}`); + console.log( + ` Email Verification Strategy: ${GLOBAL_PROPERTIES.emailVerificationStrategy}`, + ); + console.log(` CRM Backend: ${GLOBAL_PROPERTIES.crmBackend}`); + console.log(` Telemetry: ${GLOBAL_PROPERTIES.telemetry}`); + console.log("=".repeat(80)); +} diff --git a/src/core/jupiter/core/docs/component/published-doc-dir-panel.tsx b/src/core/jupiter/core/docs/component/published-doc-dir-panel.tsx index 4a1f8a248..94783eae3 100644 --- a/src/core/jupiter/core/docs/component/published-doc-dir-panel.tsx +++ b/src/core/jupiter/core/docs/component/published-doc-dir-panel.tsx @@ -60,7 +60,7 @@ export function PublishedDocDirPanel(props: PublishedDocDirPanelProps) { const { dirLoad, externalId, publishedRootDirRefId } = props; const dirId = dirLoad.dir.ref_id; - const basePath = `/app/public/published/doc/dirtree/${externalId}`; + const basePath = `/publish/doc/dirtree/${externalId}`; const [selectedTagsRefId, setSelectedTagsRefId] = useState<string[]>([]); const [sortOrder, setSortOrder] = useState<DocsSortOrder>( diff --git a/src/core/jupiter/core/infra/component/docs-help.tsx b/src/core/jupiter/core/infra/component/docs-help.tsx index b22c6d6d6..97b2cd56a 100644 --- a/src/core/jupiter/core/infra/component/docs-help.tsx +++ b/src/core/jupiter/core/infra/component/docs-help.tsx @@ -3,7 +3,7 @@ import { HelpCenter as HelpCenterIcon } from "@mui/icons-material"; import { IconButton } from "@mui/material"; import { useContext } from "react"; -import { ServicePropertiesContext } from "#/core/config-client"; +import { ServiceLinksContext } from "#/core/infra/service-links-context"; interface DocsHelpProps { size: "small" | "medium" | "large"; @@ -12,12 +12,9 @@ interface DocsHelpProps { } export function DocsHelp(props: DocsHelpProps) { - const serviceProperties = useContext(ServicePropertiesContext); + const serviceLinks = useContext(ServiceLinksContext); - const helpUrl = new URL( - subjectToUrl(props.subject), - serviceProperties.docsUrl, - ); + const helpUrl = new URL(subjectToUrl(props.subject), serviceLinks.docsUrl); return ( <IconButton diff --git a/src/core/jupiter/core/infra/component/progress-reporter.tsx b/src/core/jupiter/core/infra/component/progress-reporter.tsx index f463f069d..58a4afb5d 100644 --- a/src/core/jupiter/core/infra/component/progress-reporter.tsx +++ b/src/core/jupiter/core/infra/component/progress-reporter.tsx @@ -18,7 +18,7 @@ import useWebSocket, { ReadyState } from "react-use-websocket"; import { z } from "zod"; import { ClientOnly } from "#/core/infra/component/client-only"; -import { ServicePropertiesContext } from "#/core/config-client"; +import { ServiceLinksContext } from "#/core/infra/service-links-context"; import { useBigScreen } from "#/core/infra/component/use-big-screen"; enum LogMessageType { @@ -84,14 +84,14 @@ export default function ProgressReporter(props: ProgressReporterProps) { } function ProgressReporterClientOnly(props: ProgressReporterProps) { - const serviceProperties = useContext(ServicePropertiesContext); + const serviceLinks = useContext(ServiceLinksContext); const isBigScreen = useBigScreen(); const [showContainer, setShowContainer] = useState<boolean>(false); const [progressLog, setProgressLog] = useState<LogMessage[]>([]); const [unseenMessages, setUnseenMessages] = useState<number>(0); const webApiProgressReporterUrl = new URL( - serviceProperties.webApiProgressReporterUrl, + serviceLinks.webApiProgressReporterUrl, ); webApiProgressReporterUrl.searchParams.append("token", props.token); diff --git a/src/core/jupiter/core/infra/component/release-update-widget.tsx b/src/core/jupiter/core/infra/component/release-update-widget.tsx index 7a747c15d..bfca10c57 100644 --- a/src/core/jupiter/core/infra/component/release-update-widget.tsx +++ b/src/core/jupiter/core/infra/component/release-update-widget.tsx @@ -16,10 +16,8 @@ import { import { useFetcher } from "@remix-run/react"; import { useContext, useEffect, useState } from "react"; -import { - GlobalPropertiesContext, - ServicePropertiesContext, -} from "#/core/config-client"; +import { GlobalPropertiesContext } from "#/core/config-client"; +import { FrontDoorInfoContext } from "#/core/infra/frontdoor-info-context"; import type { ReleaseManifestResult } from "#/core/infra/release"; const REFRESH_INTERVAL_MS = 5 * 60 * 1000; @@ -27,7 +25,7 @@ const REFRESH_INTERVAL_MS = 5 * 60 * 1000; export function ReleaseUpdateWidget() { const releaseManifestFetcher = useFetcher<ReleaseManifestResult>(); const globalProperties = useContext(GlobalPropertiesContext); - const serviceProperties = useContext(ServicePropertiesContext); + const frontDoorInfo = useContext(FrontDoorInfoContext); const [dismiss, setDismiss] = useState(false); useEffect(() => { @@ -64,18 +62,17 @@ export function ReleaseUpdateWidget() { // If there is, we will show the download button. // If there isn't, we continue the analysis. if ( - releaseManifestResult.latestServerVersion !== - serviceProperties.frontDoorInfo.clientVersion + releaseManifestResult.latestServerVersion !== frontDoorInfo.clientVersion ) { let action = false; - switch (serviceProperties.frontDoorInfo.appShell) { + switch (frontDoorInfo.appShell) { case AppShell.BROWSER: case AppShell.PWA: break; case AppShell.DESKTOP_ELECTRON: - switch (serviceProperties.frontDoorInfo.appDistribution) { + switch (frontDoorInfo.appDistribution) { case AppDistribution.MAC_WEB: if ( releaseManifestResult.manifest[AppDistribution.MAC_WEB] === @@ -95,7 +92,7 @@ export function ReleaseUpdateWidget() { } break; case AppShell.MOBILE_CAPACITOR: - switch (serviceProperties.frontDoorInfo.appPlatform) { + switch (frontDoorInfo.appPlatform) { case AppPlatform.MOBILE_IOS: case AppPlatform.TABLET_IOS: if ( @@ -135,7 +132,7 @@ export function ReleaseUpdateWidget() { color="primary" component={"a"} target="_blank" - href={`/apps-latest-versions?distribution=${serviceProperties.frontDoorInfo.appDistribution}`} + href={`/apps-latest-versions?distribution=${frontDoorInfo.appDistribution}`} > Download </Button> diff --git a/src/core/jupiter/core/infra/component/use-big-screen.ts b/src/core/jupiter/core/infra/component/use-big-screen.ts index 4381cc484..c2aa752c6 100644 --- a/src/core/jupiter/core/infra/component/use-big-screen.ts +++ b/src/core/jupiter/core/infra/component/use-big-screen.ts @@ -2,16 +2,16 @@ import { AppPlatform, AppShell } from "@jupiter/webapi-client"; import { useMediaQuery, useTheme } from "@mui/material"; import { useContext } from "react"; -import { ServicePropertiesContext } from "#/core/config-client"; +import { FrontDoorInfoContext } from "#/core/infra/frontdoor-info-context"; export function useBigScreen(): boolean { - const serviceProperties = useContext(ServicePropertiesContext); + const frontDoorInfo = useContext(FrontDoorInfoContext); const theme = useTheme(); const mediaQuery = useMediaQuery(theme.breakpoints.up("md")); - switch (serviceProperties.frontDoorInfo.appShell) { + switch (frontDoorInfo.appShell) { case AppShell.BROWSER: - switch (serviceProperties.frontDoorInfo.appPlatform) { + switch (frontDoorInfo.appPlatform) { case AppPlatform.DESKTOP_MACOS: return mediaQuery; case AppPlatform.MOBILE_IOS: @@ -29,10 +29,8 @@ export function useBigScreen(): boolean { case AppShell.DESKTOP_ELECTRON: { const mdBreakpointPx = theme.breakpoints.values["md"]; - if (serviceProperties.frontDoorInfo.initialWindowWidth !== undefined) { - if ( - serviceProperties.frontDoorInfo.initialWindowWidth > mdBreakpointPx - ) { + if (frontDoorInfo.initialWindowWidth !== undefined) { + if (frontDoorInfo.initialWindowWidth > mdBreakpointPx) { return true; } else { return false; @@ -44,7 +42,7 @@ export function useBigScreen(): boolean { case AppShell.MOBILE_CAPACITOR: return false; case AppShell.PWA: - switch (serviceProperties.frontDoorInfo.appPlatform) { + switch (frontDoorInfo.appPlatform) { case AppPlatform.DESKTOP_MACOS: return mediaQuery; case AppPlatform.MOBILE_IOS: diff --git a/src/core/jupiter/core/infra/frontdoor-info-context.ts b/src/core/jupiter/core/infra/frontdoor-info-context.ts new file mode 100644 index 000000000..a90d88caf --- /dev/null +++ b/src/core/jupiter/core/infra/frontdoor-info-context.ts @@ -0,0 +1,12 @@ +import { AppDistribution, AppPlatform, AppShell } from "@jupiter/webapi-client"; +import { createContext } from "react"; + +import type { FrontDoorInfo } from "#/core/frontdoor"; + +export const FrontDoorInfoContext = createContext<FrontDoorInfo>({ + clientVersion: "FAKE-FAKE", + appShell: AppShell.BROWSER, + appPlatform: AppPlatform.DESKTOP_MACOS, + appDistribution: AppDistribution.WEB, + initialWindowWidth: undefined, +}); diff --git a/src/core/jupiter/core/infra/overdue-thresholds-context.ts b/src/core/jupiter/core/infra/overdue-thresholds-context.ts new file mode 100644 index 000000000..58b1d8ba4 --- /dev/null +++ b/src/core/jupiter/core/infra/overdue-thresholds-context.ts @@ -0,0 +1,15 @@ +import { createContext } from "react"; + +// Display thresholds for marking tasks as overdue. Each service provides +// these from its own service properties. +export interface OverdueThresholds { + overdueInfoDays: number; + overdueWarningDays: number; + overdueDangerDays: number; +} + +export const OverdueThresholdsContext = createContext<OverdueThresholds>({ + overdueInfoDays: 1, + overdueWarningDays: 2, + overdueDangerDays: 3, +}); diff --git a/src/core/jupiter/core/infra/service-links-context.ts b/src/core/jupiter/core/infra/service-links-context.ts new file mode 100644 index 000000000..3fb9ec2a2 --- /dev/null +++ b/src/core/jupiter/core/infra/service-links-context.ts @@ -0,0 +1,15 @@ +import { createContext } from "react"; + +// URLs of other services that core components link to or connect to. Each +// service provides these from its own service properties. +export interface ServiceLinks { + webApiProgressReporterUrl: string; + publishedUrl: string; + docsUrl: string; +} + +export const ServiceLinksContext = createContext<ServiceLinks>({ + webApiProgressReporterUrl: "FAKE-FAKE", + publishedUrl: "FAKE-FAKE", + docsUrl: "FAKE-FAKE", +}); diff --git a/src/core/jupiter/core/search/components/search-widget.tsx b/src/core/jupiter/core/search/components/search-widget.tsx index 13814fa3e..c4b18d42f 100644 --- a/src/core/jupiter/core/search/components/search-widget.tsx +++ b/src/core/jupiter/core/search/components/search-widget.tsx @@ -28,7 +28,7 @@ import type { ActionResult } from "#/core/infra/action-result"; import { isNoErrorSomeData } from "#/core/infra/action-result"; import { GlobalError } from "#/core/infra/component/errors"; import { TopLevelInfoContext } from "#/core/infra/top-level-context"; -import { ServicePropertiesContext } from "#/core/config-client"; +import { FrontDoorInfoContext } from "#/core/infra/frontdoor-info-context"; import { ContactsFilterPicker } from "#/core/common/sub/contacts/component/contacts-filter-picker"; import { TagsFilterPicker } from "#/core/common/sub/tags/component/tags-filter-picker"; import { useBigScreen } from "#/core/infra/component/use-big-screen"; @@ -68,9 +68,8 @@ interface SearchWidgetProps { export function SearchWidget({ allTags, allContacts }: SearchWidgetProps) { const topLevelInfo = useContext(TopLevelInfoContext); - const serviceProperties = useContext(ServicePropertiesContext); - const isMobileAppShell = - serviceProperties.frontDoorInfo.appShell === AppShell.MOBILE_CAPACITOR; + const frontDoorInfo = useContext(FrontDoorInfoContext); + const isMobileAppShell = frontDoorInfo.appShell === AppShell.MOBILE_CAPACITOR; const isBigScreen = useBigScreen(); const searchFetcher = useFetcher<WorkspaceSearchInstantResponse>(); const searchInputRef = useRef<HTMLInputElement>(null); diff --git a/src/core/jupiter/core/working_mem/use_case/load_current.py b/src/core/jupiter/core/working_mem/use_case/load_current.py index c72fc66b8..422f53573 100644 --- a/src/core/jupiter/core/working_mem/use_case/load_current.py +++ b/src/core/jupiter/core/working_mem/use_case/load_current.py @@ -1,6 +1,10 @@ """Use case for loading the current working memory file.""" from jupiter.core.common.sub.notes.root import Note, NoteRepository +from jupiter.core.common.sub.publish.sub.entity.root import ( + PublishEntity, + PublishEntityRepository, +) from jupiter.core.config import ( JupiterLoggedInReadonlyContext, JupiterTransactionalLoggedInReadOnlyUseCase, @@ -40,6 +44,7 @@ class WorkingMemLoadCurrentEntry(UseCaseResultBase): working_mem: WorkingMem note: Note + publish_entity: PublishEntity | None @use_case_result @@ -75,7 +80,17 @@ async def _perform_transactional_read( EntityLink.std(NamedEntityTag.WORKING_MEM.value, working_mem.ref_id), allow_archived=True, ) + publish_entity = await uow.get( + PublishEntityRepository + ).load_optional_for_owner( + EntityLink.std(NamedEntityTag.WORKING_MEM.value, working_mem.ref_id), + allow_archived=False, + ) return WorkingMemLoadCurrentResult( - entry=WorkingMemLoadCurrentEntry(working_mem=working_mem, note=note) + entry=WorkingMemLoadCurrentEntry( + working_mem=working_mem, + note=note, + publish_entity=publish_entity, + ) ) diff --git a/src/published/Config.project b/src/published/Config.project new file mode 100644 index 000000000..27440f15d --- /dev/null +++ b/src/published/Config.project @@ -0,0 +1,4 @@ +WEBAPI_SERVER_HOST=127.0.0.1 +WEBAPI_SERVER_PORT=8010 +PUBLISHED_URL=http://localhost:10040/ +DOCS_URL=http://localhost:8000/ diff --git a/src/published/Dockerfile b/src/published/Dockerfile new file mode 100644 index 000000000..eb724505f --- /dev/null +++ b/src/published/Dockerfile @@ -0,0 +1,49 @@ +FROM node:22.14 +LABEL maintainer='mike@get-thriving.com' + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +RUN apt-get update && \ + apt-get install -y --no-install-recommends dumb-init && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /jupiter + +ENV CI=true + +COPY LICENSE LICENSE +COPY package.json package.json +COPY pnpm-workspace.yaml pnpm-workspace.yaml +COPY pnpm-lock.yaml pnpm-lock.yaml +COPY gen gen +COPY src/Config.global src/Config.global +COPY src/core/README.md src/core/README.md +COPY src/core/package.json src/core/package.json +COPY src/core/tsconfig.json src/core/tsconfig.json +COPY src/core/jupiter/core src/core/jupiter/core +COPY src/published/README.md src/published/README.md +COPY src/published/Config.project src/published/Config.project +COPY src/published/package.json src/published/package.json +COPY src/published/tsconfig.json src/published/tsconfig.json +COPY src/published/remix.config.js src/published/remix.config.js +COPY src/published/remix.env.d.ts src/published/remix.env.d.ts +COPY src/published/app src/published/app +COPY assets/jupiter.ico src/published/public/favicon.ico +COPY assets/jupiter.png src/published/public/logo.png + +RUN npm install -g pnpm@11.0.9 && pnpm install && chown -R "$(id -u)":"$(id -g)" node_modules + +# hadolint ignore=DL3003 +RUN cd gen/ts/webapi-client && npx tsc + +ARG PORT=10040 +ENV HOST=0.0.0.0 +ENV PORT=$PORT +EXPOSE $PORT + +WORKDIR /jupiter/src/published + +RUN npx remix build + +ENTRYPOINT [ "dumb-init", "npx", "remix-serve", "build/index.js" ] diff --git a/src/published/README.md b/src/published/README.md new file mode 100644 index 000000000..ed2778ebd --- /dev/null +++ b/src/published/README.md @@ -0,0 +1,3 @@ +# Published + +Remix service that hosts and renders Thrive public (published) share pages. diff --git a/src/published/app/api-clients.server.ts b/src/published/app/api-clients.server.ts new file mode 100644 index 000000000..fcd9d20bf --- /dev/null +++ b/src/published/app/api-clients.server.ts @@ -0,0 +1,42 @@ +import { ApiClient } from "@jupiter/webapi-client"; +import { GLOBAL_PROPERTIES } from "@jupiter/core/config-server"; +import type { FrontDoorInfo } from "@jupiter/core/frontdoor"; +import { loadFrontDoorInfo } from "@jupiter/core/frontdoor.server"; +import { FRONTDOOR_HEADER, TRACE_ID_HEADER } from "@jupiter/core/infra/names"; +import { newTraceId } from "@jupiter/core/common/trace-id"; + +import { SERVICE_PROPERTIES } from "~/logic/config.server"; + +let _GUEST_API_CLIENT: ApiClient | undefined = undefined; + +// @secureFn +export async function getGuestApiClient( + request: Request, + newFrontDoor?: FrontDoorInfo, +): Promise<ApiClient> { + // The frontdoor cookie is never set on the published domain; infer + // everything from the user agent. + const frontDoor = + newFrontDoor || + (await loadFrontDoorInfo( + GLOBAL_PROPERTIES.version, + null, + request.headers.get("User-Agent"), + )); + + if (_GUEST_API_CLIENT !== undefined) { + return _GUEST_API_CLIENT; + } + + const base = SERVICE_PROPERTIES.webApiServerUrl; + + _GUEST_API_CLIENT = new ApiClient({ + BASE: base, + HEADERS: { + [FRONTDOOR_HEADER]: `${frontDoor.clientVersion}:${frontDoor.appShell}:${frontDoor.appPlatform}:${frontDoor.appDistribution}`, + [TRACE_ID_HEADER]: newTraceId(), + }, + }); + + return _GUEST_API_CLIENT; +} diff --git a/src/published/app/entry.client.tsx b/src/published/app/entry.client.tsx new file mode 100644 index 000000000..9f63fde4d --- /dev/null +++ b/src/published/app/entry.client.tsx @@ -0,0 +1,50 @@ +import { RemixBrowser } from "@remix-run/react"; +import { Buffer } from "buffer-polyfill"; +import { StrictMode, startTransition } from "react"; +import { hydrateRoot } from "react-dom/client"; + +function hydrate() { + startTransition(() => { + hydrateRoot( + document, + <StrictMode> + <RemixBrowser /> + </StrictMode>, + ); + }); +} + +document + .querySelectorAll( + [ + "html > *:not(body, head)", + 'script[src*="extension://"]', + 'link[href*="extension://"]', + ].join(", "), + ) + .forEach((s) => { + s.parentNode?.removeChild(s); + }); + +window.onerror = (event: Event | string) => { + if ( + typeof event === "string" && + (event.indexOf("Hydration failed") !== -1 || + event.indexOf("Minified React error") !== -1) + ) { + const destUrl = Buffer.from( + `${window.location.pathname}?${window.location.search}`, + "utf-8", + ).toString("base64"); + window.location.replace(`/render-fix?returnTo=${destUrl}`); + return true; + } + + return false; +}; + +if (window.requestIdleCallback) { + window.requestIdleCallback(hydrate); +} else { + window.setTimeout(hydrate, 1); +} diff --git a/src/published/app/entry.server.tsx b/src/published/app/entry.server.tsx new file mode 100644 index 000000000..b69328d02 --- /dev/null +++ b/src/published/app/entry.server.tsx @@ -0,0 +1,76 @@ +import { PassThrough, Readable } from "stream"; + +import type { EntryContext } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { renderToPipeableStream } from "react-dom/server"; +import { GLOBAL_PROPERTIES } from "@jupiter/core/config-server"; +import { + ENV_HEADER, + HOSTING_HEADER, + INSTANCE_HEADER, + UNIVERSE_HEADER, + VERSION_HEADER, +} from "@jupiter/core/infra/names"; +import { getHosting } from "#/core/universe"; + +const ABORT_DELAY = 5000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + let didError = false; + let done = false; + + const { pipe, abort } = renderToPipeableStream( + <RemixServer context={remixContext} url={request.url} />, + { + onShellReady() { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + responseHeaders.set(UNIVERSE_HEADER, GLOBAL_PROPERTIES.universe); + responseHeaders.set(ENV_HEADER, GLOBAL_PROPERTIES.env); + responseHeaders.set(INSTANCE_HEADER, GLOBAL_PROPERTIES.instance); + responseHeaders.set( + HOSTING_HEADER, + getHosting(GLOBAL_PROPERTIES.universe), + ); + responseHeaders.set(VERSION_HEADER, GLOBAL_PROPERTIES.version); + responseHeaders.set("X-Frame-Options", "DENY"); + + done = true; + + resolve( + new Response( + Readable.toWeb(body) as globalThis.ReadableStream<Uint8Array>, + { + headers: responseHeaders, + status: didError ? 500 : responseStatusCode, + }, + ), + ); + + pipe(body); + }, + onShellError(error: unknown) { + done = true; + reject(error); + }, + onError(error: unknown) { + didError = true; + console.error(error); + }, + }, + ); + + setTimeout(() => { + if (!done) { + abort(); + } + }, ABORT_DELAY); + }); +} diff --git a/src/published/app/logic/config.server.ts b/src/published/app/logic/config.server.ts new file mode 100644 index 000000000..6f1e46079 --- /dev/null +++ b/src/published/app/logic/config.server.ts @@ -0,0 +1,58 @@ +import { AppCore } from "@jupiter/webapi-client"; +import { config } from "dotenv"; +import { + GLOBAL_PROPERTIES, + logServiceStartupBanner, + resolvePublishedUrl, +} from "@jupiter/core/config-server"; +import type { FrontDoorInfo } from "@jupiter/core/frontdoor"; + +import type { ServicePropertiesClient } from "~/logic/config"; + +export interface ServicePropertiesServer { + webApiServerUrl: string; + publishedUrl: string; + docsUrl: string; +} + +// @secureFn +function loadServicePropertiesOnServer(): ServicePropertiesServer { + config({ path: `${process.cwd()}/Config.project` }); + + const webApiServerHost = process.env.WEBAPI_SERVER_HOST as string; + const webApiServerPort = parseInt( + process.env.WEBAPI_SERVER_PORT as string, + 10, + ); + + const serviceProperties = { + webApiServerUrl: `http://${webApiServerHost}:${webApiServerPort}`, + publishedUrl: resolvePublishedUrl(GLOBAL_PROPERTIES), + docsUrl: process.env.DOCS_URL as string, + }; + + return serviceProperties; +} + +export const SERVICE_PROPERTIES = loadServicePropertiesOnServer(); + +logServiceStartupBanner("published"); + +export function serverToClientServiceProperties( + servicePropertiesServer: ServicePropertiesServer, + frontDoorInfo: FrontDoorInfo, +): ServicePropertiesClient { + return { + appCore: AppCore.PUBLISHED, + frontDoorInfo: frontDoorInfo, + // The published service is read-only and never triggers mutations, so + // there is no progress reporter to connect to. + webApiProgressReporterUrl: "", + publishedUrl: servicePropertiesServer.publishedUrl, + docsUrl: servicePropertiesServer.docsUrl, + // Standard display thresholds - guests can't configure these. + overdueInfoDays: 1, + overdueWarningDays: 7, + overdueDangerDays: 21, + }; +} diff --git a/src/published/app/logic/config.ts b/src/published/app/logic/config.ts new file mode 100644 index 000000000..88a17bd18 --- /dev/null +++ b/src/published/app/logic/config.ts @@ -0,0 +1,36 @@ +import { + AppCore, + AppDistribution, + AppPlatform, + AppShell, +} from "@jupiter/webapi-client"; +import { createContext } from "react"; +import type { FrontDoorInfo } from "@jupiter/core/frontdoor"; + +export interface ServicePropertiesClient { + appCore: AppCore; + frontDoorInfo: FrontDoorInfo; + webApiProgressReporterUrl: string; + publishedUrl: string; + docsUrl: string; + overdueInfoDays: number; + overdueWarningDays: number; + overdueDangerDays: number; +} + +export const ServicePropertiesContext = createContext<ServicePropertiesClient>({ + appCore: AppCore.PUBLISHED, + frontDoorInfo: { + clientVersion: "FAKE-FAKE", + appShell: AppShell.BROWSER, + appPlatform: AppPlatform.DESKTOP_MACOS, + appDistribution: AppDistribution.WEB, + initialWindowWidth: undefined, + }, + webApiProgressReporterUrl: "FAKE-FAKE", + publishedUrl: "FAKE-FAKE", + docsUrl: "FAKE-FAKE", + overdueInfoDays: 1, + overdueWarningDays: 2, + overdueDangerDays: 3, +}); diff --git a/src/published/app/logic/navigation.ts b/src/published/app/logic/navigation.ts new file mode 100644 index 000000000..6557e94fd --- /dev/null +++ b/src/published/app/logic/navigation.ts @@ -0,0 +1,13 @@ +export function newURLParams( + query: URLSearchParams, + key: string, + value: string, + ...rest: string[] +): URLSearchParams { + const newQuery = new URLSearchParams(query.toString()); + newQuery.set(key, value); + for (let i = 0; i < rest.length; i += 2) { + newQuery.set(rest[i], rest[i + 1]); + } + return newQuery; +} diff --git a/src/webui/app/rendering/published-loader.server.ts b/src/published/app/rendering/published-loader.server.ts similarity index 100% rename from src/webui/app/rendering/published-loader.server.ts rename to src/published/app/rendering/published-loader.server.ts diff --git a/src/webui/app/rendering/published-meta.ts b/src/published/app/rendering/published-meta.ts similarity index 100% rename from src/webui/app/rendering/published-meta.ts rename to src/published/app/rendering/published-meta.ts diff --git a/src/published/app/rendering/standard-should-revalidate.ts b/src/published/app/rendering/standard-should-revalidate.ts new file mode 100644 index 000000000..99cf858c4 --- /dev/null +++ b/src/published/app/rendering/standard-should-revalidate.ts @@ -0,0 +1,83 @@ +import type { ShouldRevalidateFunction } from "@remix-run/react"; + +export const standardShouldRevalidate: ShouldRevalidateFunction = ({ + currentUrl, + defaultShouldRevalidate, + formAction, + formMethod, + nextUrl, +}) => { + if ( + formAction === "/app/workspace/core/inbox-tasks/update-status-and-eisen" + ) { + return false; + } + + if (formAction === "/app/workspace/docs/update-action") { + return false; + } + + if (formAction === "/app/workspace/core/notes/update") { + return false; + } + + if ( + currentUrl.pathname === nextUrl.pathname && + onlyDifferenceIsInTimeEventParamsSource( + formMethod || "GET", + currentUrl, + nextUrl, + ) + ) { + return false; + } + + return defaultShouldRevalidate; +}; + +function onlyDifferenceIsInTimeEventParamsSource( + formMethod: string, + currentUrl: URL, + nextUrl: URL, +): boolean { + if (formMethod !== "GET") { + return false; + } + + const currentKeys = Array.from(currentUrl.searchParams.keys()); + const nextKeys = Array.from(nextUrl.searchParams.keys()); + + if (currentKeys.length === 0 && nextKeys.length === 0) { + return false; + } + + for (const key of currentKeys) { + if ( + key === "sourceStartDate" || + key === "sourceStartTimeInDay" || + key === "sourceDurationMins" + ) { + continue; + } + + if (currentUrl.searchParams.get(key) !== nextUrl.searchParams.get(key)) { + return false; + } + } + + for (const key of nextKeys) { + if ( + key === "sourceStartDate" || + key === "sourceStartTimeInDay" || + key === "sourceDurationMins" + ) { + continue; + } + + if (currentUrl.searchParams.get(key) !== nextUrl.searchParams.get(key)) { + return false; + } + } + + return true; +} diff --git a/src/published/app/rendering/use-loader-data-for-animation.ts b/src/published/app/rendering/use-loader-data-for-animation.ts new file mode 100644 index 000000000..d28d59f40 --- /dev/null +++ b/src/published/app/rendering/use-loader-data-for-animation.ts @@ -0,0 +1,14 @@ +import type { SerializeFrom } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { useEffect, useRef } from "react"; + +export function useLoaderDataSafeForAnimation<T>(): SerializeFrom<T> { + const lastData = useRef({}); + const data = useLoaderData<T>() || lastData.current; + + useEffect(() => { + if (data) lastData.current = data; + }, [data]); + + return data as SerializeFrom<T>; +} diff --git a/src/published/app/root.tsx b/src/published/app/root.tsx new file mode 100644 index 000000000..c5b99e7a9 --- /dev/null +++ b/src/published/app/root.tsx @@ -0,0 +1,120 @@ +import { CssBaseline, ThemeProvider, createTheme } from "@mui/material"; +import type { SerializeFrom } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import type { ShouldRevalidateFunction } from "@remix-run/react"; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + useLoaderData, +} from "@remix-run/react"; +import { SnackbarProvider } from "notistack"; +import { StrictMode, useEffect, useMemo, useState } from "react"; +import { EnvBanner } from "@jupiter/core/infra/component/env-banner"; +import { serverToClientGlobalProperties } from "@jupiter/core/config-client"; +import { GLOBAL_PROPERTIES } from "@jupiter/core/config-server"; +import { getPublicName } from "#/core/utils"; + +function buildTheme(useNightMode: boolean) { + return createTheme({ + palette: { + mode: useNightMode ? "dark" : "light", + primary: { + main: "#3F51B5", + light: "#7986CB", + dark: "#303F9F", + }, + secondary: { + main: "#FF4081", + light: "#FF79B0", + dark: "#C60055", + }, + ...(!useNightMode && { + divider: "#E0E0E0", + text: { + primary: "#212121", + secondary: "#757575", + disabled: "#BDBDBD", + }, + }), + }, + typography: { + fontFamily: '"Helvetica", "Arial", sans-serif', + }, + ...(useNightMode && { + components: { + MuiCard: { + styleOverrides: { + root: { + border: "1px solid rgba(255, 255, 255, 0.12)", + }, + }, + }, + }, + }), + }); +} + +export async function loader() { + return json({ + globalProperties: serverToClientGlobalProperties(GLOBAL_PROPERTIES), + }); +} + +export function meta({ data }: { data: SerializeFrom<typeof loader> }) { + return [ + { charset: "utf-8" }, + { title: getPublicName(data.globalProperties) }, + ]; +} + +export const shouldRevalidate: ShouldRevalidateFunction = () => false; + +export default function Root() { + const loaderData = useLoaderData<typeof loader>(); + + // Guests have no stored preference - infer night mode from the OS. + const osPrefersDark = + typeof window !== "undefined" + ? window.matchMedia("(prefers-color-scheme: dark)").matches + : false; + const [systemNightMode, setSystemNightMode] = useState(osPrefersDark); + + useEffect(() => { + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + setSystemNightMode(mq.matches); + const handler = (e: MediaQueryListEvent) => setSystemNightMode(e.matches); + mq.addEventListener("change", handler); + return () => mq.removeEventListener("change", handler); + }, []); + + const theme = useMemo(() => buildTheme(systemNightMode), [systemNightMode]); + + return ( + <html lang="en"> + <head> + <meta + name="viewport" + content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no" + /> + <Meta /> + <Links /> + </head> + <body> + <StrictMode> + <ThemeProvider theme={theme}> + <SnackbarProvider> + <CssBaseline /> + <EnvBanner env={loaderData.globalProperties.env} /> + <Outlet /> + </SnackbarProvider> + </ThemeProvider> + </StrictMode> + <Scripts /> + <LiveReload /> + </body> + </html> + ); +} diff --git a/src/published/app/routes/healthz.tsx b/src/published/app/routes/healthz.tsx new file mode 100644 index 000000000..874439dd8 --- /dev/null +++ b/src/published/app/routes/healthz.tsx @@ -0,0 +1,5 @@ +import { json } from "@remix-run/node"; + +export async function loader() { + return json("OK"); +} diff --git a/src/webui/app/routes/app/public/published.tsx b/src/published/app/routes/publish.tsx similarity index 50% rename from src/webui/app/routes/app/public/published.tsx rename to src/published/app/routes/publish.tsx index 1b63937c1..1cb54dae1 100644 --- a/src/webui/app/routes/app/public/published.tsx +++ b/src/published/app/routes/publish.tsx @@ -17,15 +17,42 @@ import { Logo } from "@jupiter/core/infra/component/logo"; import { Title } from "@jupiter/core/infra/component/title"; import { TopLevelInfoProvider } from "@jupiter/core/infra/component/top-level-info-provider"; import { EMPTY_CONTEXT } from "@jupiter/core/infra/top-level-context"; +import { + GlobalPropertiesContext, + serverToClientGlobalProperties, +} from "@jupiter/core/config-client"; +import { GLOBAL_PROPERTIES } from "@jupiter/core/config-server"; +import { FrontDoorInfoContext } from "@jupiter/core/infra/frontdoor-info-context"; +import { OverdueThresholdsContext } from "@jupiter/core/infra/overdue-thresholds-context"; +import { ServiceLinksContext } from "@jupiter/core/infra/service-links-context"; +import { loadFrontDoorInfo } from "@jupiter/core/frontdoor.server"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { getGuestApiClient } from "~/api-clients.server"; +import { ServicePropertiesContext } from "~/logic/config"; +import { + SERVICE_PROPERTIES, + serverToClientServiceProperties, +} from "~/logic/config.server"; export async function loader({ request }: LoaderFunctionArgs) { + // The frontdoor cookie is never set on the published domain; infer + // everything from the user agent. + const frontDoor = await loadFrontDoorInfo( + GLOBAL_PROPERTIES.version, + null, + request.headers.get("User-Agent"), + ); + const apiClient = await getGuestApiClient(request); const response = await apiClient.application.loadTopLevelInfo({}); return json({ + globalProperties: serverToClientGlobalProperties(GLOBAL_PROPERTIES), + serviceProperties: serverToClientServiceProperties( + SERVICE_PROPERTIES, + frontDoor, + ), userFeatureFlagControls: response.user_feature_flag_controls, workspaceFeatureFlagControls: response.workspace_feature_flag_controls, userScoreOverview: response.user_score_overview ?? null, @@ -41,31 +68,47 @@ export default function Published() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); return ( - <TopLevelInfoProvider - user={loaderData.user} - workspace={loaderData.workspace} - userFeatureFlagControls={loaderData.userFeatureFlagControls} - workspaceFeatureFlagControls={loaderData.workspaceFeatureFlagControls} - userScoreOverview={loaderData.userScoreOverview} - > - <StandaloneContainer> - <SmartAppBar> - <Logo /> + <GlobalPropertiesContext.Provider value={loaderData.globalProperties}> + <ServicePropertiesContext.Provider value={loaderData.serviceProperties}> + <FrontDoorInfoContext.Provider + value={loaderData.serviceProperties.frontDoorInfo} + > + <ServiceLinksContext.Provider value={loaderData.serviceProperties}> + <OverdueThresholdsContext.Provider + value={loaderData.serviceProperties} + > + <TopLevelInfoProvider + user={loaderData.user} + workspace={loaderData.workspace} + userFeatureFlagControls={loaderData.userFeatureFlagControls} + workspaceFeatureFlagControls={ + loaderData.workspaceFeatureFlagControls + } + userScoreOverview={loaderData.userScoreOverview} + > + <StandaloneContainer> + <SmartAppBar> + <Logo /> - <Title /> + <Title /> - <CommunityLink /> + <CommunityLink /> - <DocsHelp - size="medium" - subject={DocsHelpSubject.ROOT} - theId="docs-help" - /> - </SmartAppBar> + <DocsHelp + size="medium" + subject={DocsHelpSubject.ROOT} + theId="docs-help" + /> + </SmartAppBar> - <Outlet /> - </StandaloneContainer> - </TopLevelInfoProvider> + <Outlet /> + </StandaloneContainer> + </TopLevelInfoProvider> + </OverdueThresholdsContext.Provider> + </ServiceLinksContext.Provider> + </FrontDoorInfoContext.Provider> + </ServicePropertiesContext.Provider> + </GlobalPropertiesContext.Provider> ); } diff --git a/src/webui/app/routes/app/public/published/$externalId.tsx b/src/published/app/routes/publish/$externalId.tsx similarity index 66% rename from src/webui/app/routes/app/public/published/$externalId.tsx rename to src/published/app/routes/publish/$externalId.tsx index 3facf2b7e..eba691ff2 100644 --- a/src/webui/app/routes/app/public/published/$externalId.tsx +++ b/src/published/app/routes/publish/$externalId.tsx @@ -19,39 +19,39 @@ function publishedEntityLocation(externalId: string, owner: string): string { switch (theType) { case NamedEntityTag.TODO_TASK: - return `/app/public/published/todo-task/${externalId}`; + return `/publish/todo-task/${externalId}`; case NamedEntityTag.VACATION: - return `/app/public/published/vacation/${externalId}`; + return `/publish/vacation/${externalId}`; case NamedEntityTag.JOURNAL: - return `/app/public/published/journal/${externalId}`; + return `/publish/journal/${externalId}`; case NamedEntityTag.TIME_PLAN: - return `/app/public/published/time-plan/${externalId}`; + return `/publish/time-plan/${externalId}`; case NamedEntityTag.SCHEDULE_EVENT_IN_DAY: - return `/app/public/published/schedule-event-in-day/${externalId}`; + return `/publish/schedule-event-in-day/${externalId}`; case NamedEntityTag.SCHEDULE_EVENT_FULL_DAYS: - return `/app/public/published/schedule-event-full-days/${externalId}`; + return `/publish/schedule-event-full-days/${externalId}`; case NamedEntityTag.SMART_LIST: - return `/app/public/published/smart-list/${externalId}`; + return `/publish/smart-list/${externalId}`; case NamedEntityTag.SMART_LIST_ITEM: - return `/app/public/published/smart-list/item/${externalId}`; + return `/publish/smart-list/item/${externalId}`; case NamedEntityTag.METRIC: - return `/app/public/published/metric/${externalId}`; + return `/publish/metric/${externalId}`; case NamedEntityTag.METRIC_ENTRY: - return `/app/public/published/metric/entry/${externalId}`; + return `/publish/metric/entry/${externalId}`; case NamedEntityTag.DOC: - return `/app/public/published/doc/doc/${externalId}`; + return `/publish/doc/doc/${externalId}`; case NamedEntityTag.DIR: - return `/app/public/published/doc/dir/${externalId}`; + return `/publish/doc/dir/${externalId}`; case NamedEntityTag.PERSON: - return `/app/public/published/person/${externalId}`; + return `/publish/person/${externalId}`; case NamedEntityTag.HABIT: - return `/app/public/published/habit/${externalId}`; + return `/publish/habit/${externalId}`; case NamedEntityTag.CHORE: - return `/app/public/published/chore/${externalId}`; + return `/publish/chore/${externalId}`; case NamedEntityTag.BIG_PLAN: - return `/app/public/published/big-plan/${externalId}`; + return `/publish/big-plan/${externalId}`; case NamedEntityTag.SCHEDULE_STREAM: - return `/app/public/published/schedule-stream/${externalId}`; + return `/publish/schedule-stream/${externalId}`; default: throw new Response(ReasonPhrases.NOT_FOUND, { status: StatusCodes.NOT_FOUND, @@ -81,7 +81,7 @@ export default function PublishedEntityRedirect() { return null; } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published entity ${params.externalId}!`, error: (params) => `There was an error loading published entity ${params.externalId}! Please try again!`, diff --git a/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx b/src/published/app/routes/publish/big-plan/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/big-plan/$externalId.tsx rename to src/published/app/routes/publish/big-plan/$externalId.tsx index 94c3093ac..c11dd8679 100644 --- a/src/webui/app/routes/app/public/published/big-plan/$externalId.tsx +++ b/src/published/app/routes/publish/big-plan/$externalId.tsx @@ -19,12 +19,12 @@ import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansio import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -185,7 +185,7 @@ export default function PublishedBigPlan() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published big plan ${params.externalId}!`, error: (params) => diff --git a/src/webui/app/routes/app/public/published/chore/$externalId.tsx b/src/published/app/routes/publish/chore/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/chore/$externalId.tsx rename to src/published/app/routes/publish/chore/$externalId.tsx index d52891587..dbbc66ef4 100644 --- a/src/webui/app/routes/app/public/published/chore/$externalId.tsx +++ b/src/published/app/routes/publish/chore/$externalId.tsx @@ -18,12 +18,12 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { ChoreEditor } from "@jupiter/core/chores/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -149,7 +149,7 @@ export default function PublishedChore() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published chore ${params.externalId}!`, error: (params) => `There was an error loading published chore ${params.externalId}! Please try again!`, diff --git a/src/webui/app/routes/app/public/published/doc/dir/$externalId.dir/$dirId.tsx b/src/published/app/routes/publish/doc/dir/$externalId.dir/$dirId.tsx similarity index 97% rename from src/webui/app/routes/app/public/published/doc/dir/$externalId.dir/$dirId.tsx rename to src/published/app/routes/publish/doc/dir/$externalId.dir/$dirId.tsx index c99d34c06..0a60654c8 100644 --- a/src/webui/app/routes/app/public/published/doc/dir/$externalId.dir/$dirId.tsx +++ b/src/published/app/routes/publish/doc/dir/$externalId.dir/$dirId.tsx @@ -10,13 +10,13 @@ import { makeTrunkErrorBoundary } from "@jupiter/core/infra/component/error-boun import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, publishedDirListingSummary, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -46,7 +46,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { publishEntityLoad.publish_entity.owner, ).refId; - const basePath = `/app/public/published/doc/dirtree/${externalId}`; + const basePath = `/publish/doc/dirtree/${externalId}`; const returnLocation = dirId === publishedRootDirRefId ? "/app" diff --git a/src/webui/app/routes/app/public/published/doc/dir/$externalId.tsx b/src/published/app/routes/publish/doc/dir/$externalId.tsx similarity index 88% rename from src/webui/app/routes/app/public/published/doc/dir/$externalId.tsx rename to src/published/app/routes/publish/doc/dir/$externalId.tsx index dda860062..209020406 100644 --- a/src/webui/app/routes/app/public/published/doc/dir/$externalId.tsx +++ b/src/published/app/routes/publish/doc/dir/$externalId.tsx @@ -23,7 +23,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const { refId } = parseEntityLinkStd(result.publish_entity.owner); - return redirect(`/app/public/published/doc/dirtree/${externalId}/${refId}`); + return redirect(`/publish/doc/dirtree/${externalId}/${refId}`); } catch (error) { handlePublishedLoaderError(error); } @@ -33,7 +33,7 @@ export default function PublishedDocDirRedirect() { return null; } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published folder ${params.externalId}!`, error: (params) => `There was an error loading published folder ${params.externalId}! Please try again!`, diff --git a/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId.tsx b/src/published/app/routes/publish/doc/dirtree/$externalId/$dirId.tsx similarity index 96% rename from src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId.tsx rename to src/published/app/routes/publish/doc/dirtree/$externalId/$dirId.tsx index f2f98f487..f96cf9bb0 100644 --- a/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId.tsx +++ b/src/published/app/routes/publish/doc/dirtree/$externalId/$dirId.tsx @@ -10,13 +10,13 @@ import { makeLeafErrorBoundary } from "@jupiter/core/infra/component/error-bound import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, publishedDirListingSummary, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -46,7 +46,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { publishEntityLoad.publish_entity.owner, ).refId; - const basePath = `/app/public/published/doc/dirtree/${externalId}`; + const basePath = `/publish/doc/dirtree/${externalId}`; const returnLocation = dirId === publishedRootDirRefId ? "/app" @@ -91,7 +91,7 @@ export default function PublishedDocDirView() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published folder ${params.dirId}!`, error: (params) => `There was an error loading published folder ${params.dirId}! Please try again!`, diff --git a/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId/$docId.tsx b/src/published/app/routes/publish/doc/dirtree/$externalId/$dirId/$docId.tsx similarity index 95% rename from src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId/$docId.tsx rename to src/published/app/routes/publish/doc/dirtree/$externalId/$dirId/$docId.tsx index 746f581ee..45007cde4 100644 --- a/src/webui/app/routes/app/public/published/doc/dirtree/$externalId/$dirId/$docId.tsx +++ b/src/published/app/routes/publish/doc/dirtree/$externalId/$dirId/$docId.tsx @@ -12,12 +12,12 @@ import { SectionCard } from "@jupiter/core/infra/component/section-card"; import { DisplayType } from "@jupiter/core/infra/component/use-nested-entities"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -73,7 +73,7 @@ export default function PublishedDocFromDir() { isLeaflet inputsEnabled={false} entityNotEditable={true} - returnLocation={`/app/public/published/doc/dirtree/${externalId}/${dirId}`} + returnLocation={`/publish/doc/dirtree/${externalId}/${dirId}`} > <SectionCard title="Doc"> <DocEditor @@ -97,8 +97,7 @@ export default function PublishedDocFromDir() { } export const ErrorBoundary = makeLeafErrorBoundary( - (params) => - `/app/public/published/doc/dirtree/${params.externalId}/${params.dirId}`, + (params) => `/publish/doc/dirtree/${params.externalId}/${params.dirId}`, ParamsSchema, { notFound: (params) => `Could not find published doc ${params.docId}!`, diff --git a/src/webui/app/routes/app/public/published/doc/doc/$externalId.tsx b/src/published/app/routes/publish/doc/doc/$externalId.tsx similarity index 97% rename from src/webui/app/routes/app/public/published/doc/doc/$externalId.tsx rename to src/published/app/routes/publish/doc/doc/$externalId.tsx index aaede469c..46ea2ce5f 100644 --- a/src/webui/app/routes/app/public/published/doc/doc/$externalId.tsx +++ b/src/published/app/routes/publish/doc/doc/$externalId.tsx @@ -13,12 +13,12 @@ import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansio import { DocEditor } from "@jupiter/core/docs/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -93,7 +93,7 @@ export default function PublishedDoc() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published doc ${params.externalId}!`, error: (params) => `There was an error loading published doc ${params.externalId}! Please try again!`, diff --git a/src/webui/app/routes/app/public/published/habit/$externalId.tsx b/src/published/app/routes/publish/habit/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/habit/$externalId.tsx rename to src/published/app/routes/publish/habit/$externalId.tsx index d3ffac0a9..ad0fb69dd 100644 --- a/src/webui/app/routes/app/public/published/habit/$externalId.tsx +++ b/src/published/app/routes/publish/habit/$externalId.tsx @@ -19,12 +19,12 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { HabitEditor } from "@jupiter/core/habits/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -169,7 +169,7 @@ export default function PublishedHabit() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published habit ${params.externalId}!`, error: (params) => `There was an error loading published habit ${params.externalId}! Please try again!`, diff --git a/src/webui/app/routes/app/public/published/journal/$externalId.tsx b/src/published/app/routes/publish/journal/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/journal/$externalId.tsx rename to src/published/app/routes/publish/journal/$externalId.tsx index 4bd121da5..b815f2aa2 100644 --- a/src/webui/app/routes/app/public/published/journal/$externalId.tsx +++ b/src/published/app/routes/publish/journal/$externalId.tsx @@ -17,12 +17,12 @@ import { allowUserChanges } from "@jupiter/core/journals/source"; import { ShowReport } from "@jupiter/core/report/component/show-report"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -109,7 +109,7 @@ export default function PublishedJournal() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published journal ${params.externalId}!`, error: (params) => diff --git a/src/webui/app/routes/app/public/published/metric/$externalId.tsx b/src/published/app/routes/publish/metric/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/metric/$externalId.tsx rename to src/published/app/routes/publish/metric/$externalId.tsx index d9b922c76..135dbb1ed 100644 --- a/src/webui/app/routes/app/public/published/metric/$externalId.tsx +++ b/src/published/app/routes/publish/metric/$externalId.tsx @@ -42,12 +42,12 @@ import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansio import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -256,7 +256,7 @@ export default function PublishedMetric() { entityId={`metric-entry-${entry.ref_id}`} > <EntityLink - to={`/app/public/published/metric/${loaderData.externalId}/${entry.ref_id}`} + to={`/publish/metric/${loaderData.externalId}/${entry.ref_id}`} > <EntityNameComponent name={metricEntryName(entry)} /> {indicator && ( @@ -301,7 +301,7 @@ export default function PublishedMetric() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published metric ${params.externalId}!`, error: (params) => `There was an error loading published metric ${params.externalId}! Please try again!`, diff --git a/src/webui/app/routes/app/public/published/metric/$externalId/$entryId.tsx b/src/published/app/routes/publish/metric/$externalId/$entryId.tsx similarity index 96% rename from src/webui/app/routes/app/public/published/metric/$externalId/$entryId.tsx rename to src/published/app/routes/publish/metric/$externalId/$entryId.tsx index 24e9850c3..151b97ce0 100644 --- a/src/webui/app/routes/app/public/published/metric/$externalId/$entryId.tsx +++ b/src/published/app/routes/publish/metric/$externalId/$entryId.tsx @@ -15,12 +15,12 @@ import { MetricEntryEditor } from "@jupiter/core/metrics/sub/entry/component/edi import { metricEntryName } from "@jupiter/core/metrics/sub/entry/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -75,7 +75,7 @@ export default function PublishedMetricEntryFromMetric() { isLeaflet inputsEnabled={false} entityNotEditable={true} - returnLocation={`/app/public/published/metric/${loaderData.externalId}`} + returnLocation={`/publish/metric/${loaderData.externalId}`} > <MetricEntryEditor metricEntry={metricEntry} @@ -101,7 +101,7 @@ export default function PublishedMetricEntryFromMetric() { } export const ErrorBoundary = makeLeafErrorBoundary( - (params) => `/app/public/published/metric/${params.externalId}`, + (params) => `/publish/metric/${params.externalId}`, ParamsSchema, { notFound: (params) => diff --git a/src/webui/app/routes/app/public/published/metric/entry/$externalId.tsx b/src/published/app/routes/publish/metric/entry/$externalId.tsx similarity index 97% rename from src/webui/app/routes/app/public/published/metric/entry/$externalId.tsx rename to src/published/app/routes/publish/metric/entry/$externalId.tsx index 37f91dadc..55c5b3f15 100644 --- a/src/webui/app/routes/app/public/published/metric/entry/$externalId.tsx +++ b/src/published/app/routes/publish/metric/entry/$externalId.tsx @@ -16,12 +16,12 @@ import { MetricEntryEditor } from "@jupiter/core/metrics/sub/entry/component/edi import { metricEntryName } from "@jupiter/core/metrics/sub/entry/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -100,7 +100,7 @@ export default function PublishedMetricEntry() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published metric entry ${params.externalId}!`, error: (params) => diff --git a/src/webui/app/routes/app/public/published/person/$externalId.tsx b/src/published/app/routes/publish/person/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/person/$externalId.tsx rename to src/published/app/routes/publish/person/$externalId.tsx index 74cf6f3b9..6a2b10e7d 100644 --- a/src/webui/app/routes/app/public/published/person/$externalId.tsx +++ b/src/published/app/routes/publish/person/$externalId.tsx @@ -16,12 +16,12 @@ import { PersonEditor } from "@jupiter/core/prm/sub/person/component/editor"; import { OccasionStack } from "@jupiter/core/prm/sub/person/sub/occasion/components/stack"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -125,7 +125,7 @@ export default function PublishedPerson() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published person ${params.externalId}!`, error: (params) => `There was an error loading published person ${params.externalId}! Please try again!`, diff --git a/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx b/src/published/app/routes/publish/schedule-event-full-days/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx rename to src/published/app/routes/publish/schedule-event-full-days/$externalId.tsx index 60138c313..dc08c0e63 100644 --- a/src/webui/app/routes/app/public/published/schedule-event-full-days/$externalId.tsx +++ b/src/published/app/routes/publish/schedule-event-full-days/$externalId.tsx @@ -16,12 +16,12 @@ import { ScheduleEventFullDaysEditor } from "@jupiter/core/schedule/sub/event_fu import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_full_days/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -112,7 +112,7 @@ export default function PublishedScheduleEventFullDays() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published schedule event ${params.externalId}!`, error: (params) => diff --git a/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx b/src/published/app/routes/publish/schedule-event-in-day/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx rename to src/published/app/routes/publish/schedule-event-in-day/$externalId.tsx index f71991ae9..90e9e0c3b 100644 --- a/src/webui/app/routes/app/public/published/schedule-event-in-day/$externalId.tsx +++ b/src/published/app/routes/publish/schedule-event-in-day/$externalId.tsx @@ -17,12 +17,12 @@ import { ScheduleEventInDayEditor } from "@jupiter/core/schedule/sub/event_in_da import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_in_day/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -128,7 +128,7 @@ export default function PublishedScheduleEventInDay() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published schedule event ${params.externalId}!`, error: (params) => diff --git a/src/webui/app/routes/app/public/published/schedule-stream/$externalId.tsx b/src/published/app/routes/publish/schedule-stream/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/schedule-stream/$externalId.tsx rename to src/published/app/routes/publish/schedule-stream/$externalId.tsx index 7c150dde2..8d26ef6ed 100644 --- a/src/webui/app/routes/app/public/published/schedule-stream/$externalId.tsx +++ b/src/published/app/routes/publish/schedule-stream/$externalId.tsx @@ -42,14 +42,14 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { inferPlatformAndDistribution } from "@jupiter/core/frontdoor.server"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; +import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; +import { newURLParams } from "~/logic/navigation"; +import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; -import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; -import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; -import { newURLParams } from "~/logic/navigation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -171,7 +171,7 @@ export default function PublishedScheduleStream() { const calendarLocation = ""; const isAdding = false; - const basePath = `/app/public/published/schedule-stream/${loaderData.externalId}`; + const basePath = `/publish/schedule-stream/${loaderData.externalId}`; const calendarNavigation = useMemo( () => publishedScheduleStreamCalendarNavigation(loaderData.externalId), @@ -320,7 +320,7 @@ export default function PublishedScheduleStream() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published schedule stream ${params.externalId}!`, error: (params) => diff --git a/src/webui/app/routes/app/public/published/schedule-stream/$externalId/full-days-event/$eventId.tsx b/src/published/app/routes/publish/schedule-stream/$externalId/full-days-event/$eventId.tsx similarity index 96% rename from src/webui/app/routes/app/public/published/schedule-stream/$externalId/full-days-event/$eventId.tsx rename to src/published/app/routes/publish/schedule-stream/$externalId/full-days-event/$eventId.tsx index 32e6e4605..2815dad69 100644 --- a/src/webui/app/routes/app/public/published/schedule-stream/$externalId/full-days-event/$eventId.tsx +++ b/src/published/app/routes/publish/schedule-stream/$externalId/full-days-event/$eventId.tsx @@ -16,12 +16,12 @@ import { ScheduleEventFullDaysEditor } from "@jupiter/core/schedule/sub/event_fu import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_full_days/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -81,7 +81,7 @@ export default function PublishedScheduleStreamFullDaysEvent() { isLeaflet inputsEnabled={false} entityNotEditable={true} - returnLocation={`/app/public/published/schedule-stream/${loaderData.externalId}?${query}`} + returnLocation={`/publish/schedule-stream/${loaderData.externalId}?${query}`} > <ScheduleEventFullDaysEditor scheduleEventFullDays={loaderData.scheduleEventFullDays} @@ -115,7 +115,7 @@ export default function PublishedScheduleStreamFullDaysEvent() { } export const ErrorBoundary = makeLeafErrorBoundary( - (params) => `/app/public/published/schedule-stream/${params.externalId}`, + (params) => `/publish/schedule-stream/${params.externalId}`, ParamsSchema, { notFound: (params) => diff --git a/src/webui/app/routes/app/public/published/schedule-stream/$externalId/in-day-event/$eventId.tsx b/src/published/app/routes/publish/schedule-stream/$externalId/in-day-event/$eventId.tsx similarity index 96% rename from src/webui/app/routes/app/public/published/schedule-stream/$externalId/in-day-event/$eventId.tsx rename to src/published/app/routes/publish/schedule-stream/$externalId/in-day-event/$eventId.tsx index a1706ec15..864669f62 100644 --- a/src/webui/app/routes/app/public/published/schedule-stream/$externalId/in-day-event/$eventId.tsx +++ b/src/published/app/routes/publish/schedule-stream/$externalId/in-day-event/$eventId.tsx @@ -17,12 +17,12 @@ import { ScheduleEventInDayEditor } from "@jupiter/core/schedule/sub/event_in_da import { isCorePropertyEditable } from "@jupiter/core/schedule/sub/event_in_day/root"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -92,7 +92,7 @@ export default function PublishedScheduleStreamInDayEvent() { isLeaflet inputsEnabled={false} entityNotEditable={true} - returnLocation={`/app/public/published/schedule-stream/${loaderData.externalId}?${query}`} + returnLocation={`/publish/schedule-stream/${loaderData.externalId}?${query}`} > <ScheduleEventInDayEditor scheduleEventInDay={loaderData.scheduleEventInDay} @@ -129,7 +129,7 @@ export default function PublishedScheduleStreamInDayEvent() { } export const ErrorBoundary = makeLeafErrorBoundary( - (params) => `/app/public/published/schedule-stream/${params.externalId}`, + (params) => `/publish/schedule-stream/${params.externalId}`, ParamsSchema, { notFound: (params) => diff --git a/src/webui/app/routes/app/public/published/smart-list/$externalId.tsx b/src/published/app/routes/publish/smart-list/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/smart-list/$externalId.tsx rename to src/published/app/routes/publish/smart-list/$externalId.tsx index a0529eec4..7cf0b4094 100644 --- a/src/webui/app/routes/app/public/published/smart-list/$externalId.tsx +++ b/src/published/app/routes/publish/smart-list/$externalId.tsx @@ -33,12 +33,12 @@ import { LeafPanelExpansionState } from "@jupiter/core/infra/leaf-panel-expansio import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -208,7 +208,7 @@ export default function PublishedSmartList() { entityId={`smart-list-item-${item.ref_id}`} > <EntityLink - to={`/app/public/published/smart-list/${loaderData.externalId}/${item.ref_id}`} + to={`/publish/smart-list/${loaderData.externalId}/${item.ref_id}`} > <EntityNameComponent name={item.name} /> <Check isDone={item.is_done} /> @@ -236,7 +236,7 @@ export default function PublishedSmartList() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published smart list ${params.externalId}!`, error: (params) => diff --git a/src/webui/app/routes/app/public/published/smart-list/$externalId/$itemId.tsx b/src/published/app/routes/publish/smart-list/$externalId/$itemId.tsx similarity index 95% rename from src/webui/app/routes/app/public/published/smart-list/$externalId/$itemId.tsx rename to src/published/app/routes/publish/smart-list/$externalId/$itemId.tsx index bee7eff63..0f63bf546 100644 --- a/src/webui/app/routes/app/public/published/smart-list/$externalId/$itemId.tsx +++ b/src/published/app/routes/publish/smart-list/$externalId/$itemId.tsx @@ -14,12 +14,12 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { SmartListItemEditor } from "@jupiter/core/smart_lists/sub/item/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -75,7 +75,7 @@ export default function PublishedSmartListItemFromList() { isLeaflet inputsEnabled={false} entityNotEditable={true} - returnLocation={`/app/public/published/smart-list/${loaderData.externalId}`} + returnLocation={`/publish/smart-list/${loaderData.externalId}`} > <SmartListItemEditor item={item} @@ -101,7 +101,7 @@ export default function PublishedSmartListItemFromList() { } export const ErrorBoundary = makeLeafErrorBoundary( - (params) => `/app/public/published/smart-list/${params.externalId}`, + (params) => `/publish/smart-list/${params.externalId}`, ParamsSchema, { notFound: (params) => diff --git a/src/webui/app/routes/app/public/published/smart-list/item/$externalId.tsx b/src/published/app/routes/publish/smart-list/item/$externalId.tsx similarity index 97% rename from src/webui/app/routes/app/public/published/smart-list/item/$externalId.tsx rename to src/published/app/routes/publish/smart-list/item/$externalId.tsx index e63a6942d..adbd2b68d 100644 --- a/src/webui/app/routes/app/public/published/smart-list/item/$externalId.tsx +++ b/src/published/app/routes/publish/smart-list/item/$externalId.tsx @@ -15,12 +15,12 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { SmartListItemEditor } from "@jupiter/core/smart_lists/sub/item/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -99,7 +99,7 @@ export default function PublishedSmartListItem() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published smart list item ${params.externalId}!`, error: (params) => diff --git a/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx b/src/published/app/routes/publish/time-plan/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/time-plan/$externalId.tsx rename to src/published/app/routes/publish/time-plan/$externalId.tsx index 39b7a624b..f0e894b1f 100644 --- a/src/webui/app/routes/app/public/published/time-plan/$externalId.tsx +++ b/src/published/app/routes/publish/time-plan/$externalId.tsx @@ -28,12 +28,12 @@ import { allowUserChanges } from "@jupiter/core/time_plans/source"; import { TimePlanListMergedActivities } from "@jupiter/core/time_plans/component/list-merged-activities"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -186,7 +186,7 @@ export default function PublishedTimePlan() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published time plan ${params.externalId}!`, error: (params) => diff --git a/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx b/src/published/app/routes/publish/todo-task/$externalId.tsx similarity index 98% rename from src/webui/app/routes/app/public/published/todo-task/$externalId.tsx rename to src/published/app/routes/publish/todo-task/$externalId.tsx index 6f97e1798..9079d2473 100644 --- a/src/webui/app/routes/app/public/published/todo-task/$externalId.tsx +++ b/src/published/app/routes/publish/todo-task/$externalId.tsx @@ -15,12 +15,12 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { TodoTaskPropertiesEditor } from "@jupiter/core/todo/components/properties-editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -112,7 +112,7 @@ export default function PublishedTodoTask() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published todo task ${params.externalId}!`, error: (params) => diff --git a/src/webui/app/routes/app/public/published/vacation/$externalId.tsx b/src/published/app/routes/publish/vacation/$externalId.tsx similarity index 97% rename from src/webui/app/routes/app/public/published/vacation/$externalId.tsx rename to src/published/app/routes/publish/vacation/$externalId.tsx index bafb5f99a..0506bfdd2 100644 --- a/src/webui/app/routes/app/public/published/vacation/$externalId.tsx +++ b/src/published/app/routes/publish/vacation/$externalId.tsx @@ -15,12 +15,12 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { VacationEditor } from "@jupiter/core/vacations/component/editor"; import { getGuestApiClient } from "~/api-clients.server"; +import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { buildPublishedPageMeta, metaDescriptorsForPublishedPage, } from "~/rendering/published-meta"; import { handlePublishedLoaderError } from "~/rendering/published-loader.server"; -import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; const ParamsSchema = z.object({ externalId: z.string(), @@ -100,7 +100,7 @@ export default function PublishedVacation() { ); } -export const ErrorBoundary = makeLeafErrorBoundary("/app", ParamsSchema, { +export const ErrorBoundary = makeLeafErrorBoundary("/publish", ParamsSchema, { notFound: (params) => `Could not find published vacation ${params.externalId}!`, error: (params) => diff --git a/src/published/package.json b/src/published/package.json new file mode 100644 index 000000000..3e0534efc --- /dev/null +++ b/src/published/package.json @@ -0,0 +1,59 @@ +{ + "private": true, + "sideEffects": false, + "name": "@jupiter/published", + "access": "public", + "version": "0.1.0", + "description": "The Thrive published pages service", + "author": "Mike Bestcat <mike@get-thriving.com>", + "homepage": "https://get-thriving.com", + "repository": "https://github.com/get-thriving/thrive", + "documentation": "https://docs.get-thriving.com", + "license": "MIT", + "readme": "README.md", + "dependencies": { + "@calumk/editorjs-codecup": "^1.3.0", + "@editorjs/checklist": "^1.6.0", + "@editorjs/delimiter": "^1.4.2", + "@editorjs/editorjs": "^2.31.1", + "@editorjs/header": "^2.8.8", + "@editorjs/nested-list": "^1.4.3", + "@editorjs/quote": "^2.7.6", + "@editorjs/table": "^2.4.4", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@jupiter/core": "workspace:*", + "@jupiter/webapi-client": "workspace:*", + "@mui/icons-material": "^6.4.8", + "@mui/material": "^6.4.8", + "@nivo/calendar": "=0.80.0", + "@nivo/core": "=0.80.0", + "@nivo/line": "=0.80.0", + "@remix-run/node": "^2.16.3", + "@remix-run/react": "^2.16.3", + "@remix-run/serve": "^2.16.3", + "buffer-polyfill": "npm:buffer@^6.0.3", + "dotenv": "^16.6.1", + "editorjs-drag-drop": "^1.1.16", + "framer-motion": "^12.5.0", + "http-status-codes": "^2.3.0", + "isbot": "^5.1.25", + "luxon": "^3.5.0", + "notistack": "^3.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "zod": "^3.24.2", + "zodix": "^0.4.4" + }, + "devDependencies": { + "@remix-run/dev": "^2.16.3", + "@remix-run/v1-route-convention": "^0.1.4", + "@types/luxon": "^3.4.2", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.8", + "vite": "^6.4.1" + }, + "engines": { + "node": ">=14" + } +} diff --git a/src/published/package.mise.toml b/src/published/package.mise.toml new file mode 100644 index 000000000..77361876f --- /dev/null +++ b/src/published/package.mise.toml @@ -0,0 +1,107 @@ +["package:src:published:prepare"] +hide = true +run = "true" + +["package:src:published:fix:prettier"] +hide = true +sources = ["src/published/**/*.ts", "src/published/**/*.tsx", "src/published/**/*.js", "src/published/**/*.jsx", "src/published/**/*.json", "src/published/**/*.md"] +outputs = { auto = true } +run = "npx prettier --write --list-different src/published" + +["package:src:published:fix:eslint"] +hide = true +sources = ["src/published/**/*.ts", "src/published/**/*.tsx", "src/published/**/*.js", "src/published/**/*.jsx"] +outputs = { auto = true } +run = "npx eslint src/published --fix" + +["package:src:published:fix:markdownlint"] +hide = true +sources = ["src/published/README.md"] +outputs = { auto = true } +run = "./tasks/_resources/check/lint/markdownlint-cli2.sh --fix src/published/README.md" + +["package:src:published:fix"] +description = "Fix src/published module" +depends = ["package:src:published:fix:*"] +run = "true" + +["package:src:published:lint:hadolint"] +hide = true +sources = ["src/published/Dockerfile"] +outputs = { auto = true } +run = ''' +#!/usr/bin/env bash +set -e -o pipefail +docker run --rm -i \ + -v "$(pwd)/tasks/_resources/check/lint/hadolint":/hadolint \ + -v "$(pwd)/src/published/Dockerfile:/Dockerfile" \ + hadolint/hadolint:latest-debian \ + hadolint \ + --config=/hadolint \ + /Dockerfile +''' + +["package:src:published:lint:toml"] +hide = true +sources = ["src/published/package.mise.toml"] +outputs = { auto = true } +run = "npx eslint src/published/package.mise.toml" + +["package:src:published:lint:tsc"] +hide = true +sources = ["src/published/**/*.ts", "src/published/**/*.tsx"] +outputs = { auto = true } +run = "cd src/published && npx tsc" + +["package:src:published:lint:prettier"] +hide = true +sources = [ + "src/published/**/*.ts", + "src/published/**/*.tsx", + "src/published/**/*.js", + "src/published/**/*.jsx", + "src/published/**/*.json", + "src/published/**/*.md", +] +outputs = { auto = true } +run = "npx prettier --check src/published" + +["package:src:published:lint:eslint"] +hide = true +sources = [ + "src/published/**/*.ts", + "src/published/**/*.tsx", + "src/published/**/*.js", + "src/published/**/*.jsx", +] +outputs = { auto = true } +run = "npx eslint src/published" + +["package:src:published:lint:markdownlint-readme"] +hide = true +sources = ["src/published/README.md"] +outputs = { auto = true } +run = "./tasks/_resources/check/lint/markdownlint-cli2.sh src/published/README.md" + +["package:src:published:lint"] +description = "Check src/published module" +depends = ["package:src:published:lint:*"] +run = "true" + +["package:src:published:audit:pnpm-audit"] +hide = true +run = "cd src/published && pnpm audit" + +["package:src:published:audit"] +description = "Audit src/published module" +depends = ["package:src:published:audit:*"] +run = "true" + +["package:src:published:libyear:pnpm-libyear"] +hide = true +run = "cd src/published && npx libyear" + +["package:src:published:libyear"] +description = "Check src/published libyear" +depends = ["package:src:published:libyear:*"] +run = "true" diff --git a/src/published/remix.config.js b/src/published/remix.config.js new file mode 100644 index 000000000..0e83f8dd9 --- /dev/null +++ b/src/published/remix.config.js @@ -0,0 +1,14 @@ +const { createRoutesFromFolders } = require("@remix-run/v1-route-convention"); + +/** @type {import('@remix-run/dev').AppConfig} */ +const config = { + ignoredRouteFiles: ["**/.*"], + serverModuleFormat: "cjs", + routes(defineRoutes) { + return createRoutesFromFolders(defineRoutes); + }, + watchPaths: ["../core"], + serverDependenciesToBundle: [/^@jupiter\/core(\/.*)?$/], +}; + +module.exports = config; diff --git a/src/published/remix.env.d.ts b/src/published/remix.env.d.ts new file mode 100644 index 000000000..dcf8c45e1 --- /dev/null +++ b/src/published/remix.env.d.ts @@ -0,0 +1,2 @@ +/// <reference types="@remix-run/dev" /> +/// <reference types="@remix-run/node" /> diff --git a/src/published/tsconfig.json b/src/published/tsconfig.json new file mode 100644 index 000000000..6f14a0a48 --- /dev/null +++ b/src/published/tsconfig.json @@ -0,0 +1,28 @@ +{ + "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx", "../core/editorjs.d.ts"], + "compilerOptions": { + "types": ["@remix-run/node", "vite/client"], + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2020"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "jsxImportSource": "@emotion/react", + "resolveJsonModule": true, + "target": "ES2020", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"], + "#/core/*": ["../core/jupiter/core/*"], + "@jupiter/core/*": ["../core/jupiter/core/*"] + }, + "noEmit": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } +} diff --git a/src/webui/Config.project b/src/webui/Config.project index 573edd2ca..e891d636c 100644 --- a/src/webui/Config.project +++ b/src/webui/Config.project @@ -5,6 +5,7 @@ WEBAPI_URL=http://localhost:8004/ API_URL=http://localhost:8020/ MCP_URL=http://localhost:8030/ WEBUI_URL=http://localhost:10020/ +PUBLISHED_URL=http://localhost:10040/ DOCS_URL=http://localhost:8000/ PWA_START_URL=/frontdoor?clientVersion=to-fill&appShell=pwa&appPlatform=to-fill&appDistribution=web SESSION_COOKIE_SECURE=false diff --git a/src/webui/app/api-clients.server.ts b/src/webui/app/api-clients.server.ts index 948c98f91..0cbd3cd37 100644 --- a/src/webui/app/api-clients.server.ts +++ b/src/webui/app/api-clients.server.ts @@ -1,9 +1,6 @@ import { ApiClient } from "@jupiter/webapi-client"; import { redirect } from "@remix-run/node"; -import { - GLOBAL_PROPERTIES, - SERVICE_PROPERTIES, -} from "@jupiter/core/config-server"; +import { GLOBAL_PROPERTIES } from "@jupiter/core/config-server"; import type { FrontDoorInfo } from "@jupiter/core/frontdoor"; import { loadFrontDoorInfo } from "@jupiter/core/frontdoor.server"; import { @@ -13,6 +10,7 @@ import { } from "@jupiter/core/infra/names"; import { newTraceId } from "@jupiter/core/common/trace-id"; +import { SERVICE_PROPERTIES } from "~/logic/config.server"; import { getSession } from "~/sessions"; const _API_CLIENTS_BY_SESSION = new Map<undefined | string, ApiClient>(); diff --git a/src/webui/app/logic/config.server.ts b/src/webui/app/logic/config.server.ts new file mode 100644 index 000000000..6902ce25c --- /dev/null +++ b/src/webui/app/logic/config.server.ts @@ -0,0 +1,96 @@ +import { AppCore } from "@jupiter/webapi-client"; +import { config } from "dotenv"; +import { + GLOBAL_PROPERTIES, + logServiceStartupBanner, + resolvePublishedUrl, + resolveWebUiUrl, +} from "@jupiter/core/config-server"; +import type { FrontDoorInfo } from "@jupiter/core/frontdoor"; + +import type { ServicePropertiesClient } from "~/logic/config"; + +export interface ServicePropertiesServer { + webApiServerUrl: string; + webApiProgressReporterUrl: string; + webApiUrl: string; + apiUrl: string; + mcpUrl: string; + webUiUrl: string; + publishedUrl: string; + docsUrl: string; + pwaStartUrl: string; + sessionCookieSecure: boolean; + sessionCookieSecret: string; + inboxTasksToAskForGC: number; + overdueInfoDays: number; + overdueWarningDays: number; + overdueDangerDays: number; +} + +// @secureFn +function loadServicePropertiesOnServer(): ServicePropertiesServer { + config({ path: `${process.cwd()}/Config.project` }); + + const webApiServerHost = process.env.WEBAPI_SERVER_HOST as string; + const webApiServerPort = parseInt( + process.env.WEBAPI_SERVER_PORT as string, + 10, + ); + + const webApiServerUrl = `http://${webApiServerHost}:${webApiServerPort}`; + const webApiProgressReporterUrl = process.env + .WEBAPI_PROGRESS_REPORTER_URL as string; + + const serviceProperties = { + webApiServerUrl: webApiServerUrl, + webApiProgressReporterUrl: webApiProgressReporterUrl, + webApiUrl: process.env.WEBAPI_URL as string, + apiUrl: process.env.API_URL as string, + mcpUrl: process.env.MCP_URL as string, + webUiUrl: resolveWebUiUrl(GLOBAL_PROPERTIES), + publishedUrl: resolvePublishedUrl(GLOBAL_PROPERTIES), + docsUrl: process.env.DOCS_URL as string, + pwaStartUrl: process.env.PWA_START_URL as string, + sessionCookieSecure: process.env.SESSION_COOKIE_SECURE === "true", + sessionCookieSecret: process.env.SESSION_COOKIE_SECRET as string, + inboxTasksToAskForGC: parseInt( + process.env.INBOX_TASKS_TO_ASK_FOR_GC as string, + 10, + ), + overdueInfoDays: parseInt(process.env.OVERDUE_INFO_DAYS as string, 10), + overdueWarningDays: parseInt( + process.env.OVERDUE_WARNING_DAYS as string, + 10, + ), + overdueDangerDays: parseInt(process.env.OVERDUE_DANGER_DAYS as string, 10), + }; + + return serviceProperties; +} + +export const SERVICE_PROPERTIES = loadServicePropertiesOnServer(); + +logServiceStartupBanner("webui"); + +export function serverToClientServiceProperties( + servicePropertiesServer: ServicePropertiesServer, + frontDoorInfo: FrontDoorInfo, +): ServicePropertiesClient { + return { + appCore: AppCore.WEBUI, + frontDoorInfo: frontDoorInfo, + webApiProgressReporterUrl: + servicePropertiesServer.webApiProgressReporterUrl, + webApiUrl: servicePropertiesServer.webApiUrl, + apiUrl: servicePropertiesServer.apiUrl, + mcpUrl: servicePropertiesServer.mcpUrl, + webUiUrl: servicePropertiesServer.webUiUrl, + publishedUrl: servicePropertiesServer.publishedUrl, + docsUrl: servicePropertiesServer.docsUrl, + inboxTasksToAskForGC: servicePropertiesServer.inboxTasksToAskForGC, + overdueInfoDays: servicePropertiesServer.overdueInfoDays, + overdueWarningDays: servicePropertiesServer.overdueWarningDays, + overdueDangerDays: servicePropertiesServer.overdueDangerDays, + }; +} diff --git a/src/webui/app/logic/config.ts b/src/webui/app/logic/config.ts new file mode 100644 index 000000000..e582a7449 --- /dev/null +++ b/src/webui/app/logic/config.ts @@ -0,0 +1,50 @@ +import { + AppCore, + AppDistribution, + AppPlatform, + AppShell, +} from "@jupiter/webapi-client"; +import { createContext, useContext } from "react"; +import type { FrontDoorInfo } from "@jupiter/core/frontdoor"; + +export interface ServicePropertiesClient { + appCore: AppCore; + frontDoorInfo: FrontDoorInfo; + webApiProgressReporterUrl: string; + webApiUrl: string; + apiUrl: string; + mcpUrl: string; + webUiUrl: string; + publishedUrl: string; + docsUrl: string; + inboxTasksToAskForGC: number; + overdueInfoDays: number; + overdueWarningDays: number; + overdueDangerDays: number; +} + +export const ServicePropertiesContext = createContext<ServicePropertiesClient>({ + appCore: AppCore.WEBUI, + frontDoorInfo: { + clientVersion: "FAKE-FAKE", + appShell: AppShell.BROWSER, + appPlatform: AppPlatform.DESKTOP_MACOS, + appDistribution: AppDistribution.WEB, + initialWindowWidth: undefined, + }, + webApiProgressReporterUrl: "FAKE-FAKE", + webApiUrl: "FAKE-FAKE", + apiUrl: "FAKE-FAKE", + mcpUrl: "FAKE-FAKE", + webUiUrl: "FAKE-FAKE", + publishedUrl: "FAKE-FAKE", + docsUrl: "FAKE-FAKE", + inboxTasksToAskForGC: 20, + overdueInfoDays: 1, + overdueWarningDays: 2, + overdueDangerDays: 3, +}); + +export function useServiceProperties(): ServicePropertiesClient { + return useContext(ServicePropertiesContext); +} diff --git a/src/webui/app/routes/app.tsx b/src/webui/app/routes/app.tsx index 66ba2bc96..2986dfbbd 100644 --- a/src/webui/app/routes/app.tsx +++ b/src/webui/app/routes/app.tsx @@ -9,14 +9,18 @@ import { useEffect } from "react"; import { GlobalPropertiesContext, serverToClientGlobalProperties, - serverToClientServiceProperties, - ServicePropertiesContext, } from "@jupiter/core/config-client"; +import { GLOBAL_PROPERTIES } from "@jupiter/core/config-server"; +import { FrontDoorInfoContext } from "@jupiter/core/infra/frontdoor-info-context"; +import { OverdueThresholdsContext } from "@jupiter/core/infra/overdue-thresholds-context"; +import { ServiceLinksContext } from "@jupiter/core/infra/service-links-context"; +import { loadFrontDoorInfo } from "@jupiter/core/frontdoor.server"; + +import { ServicePropertiesContext } from "~/logic/config"; import { - GLOBAL_PROPERTIES, SERVICE_PROPERTIES, -} from "@jupiter/core/config-server"; -import { loadFrontDoorInfo } from "@jupiter/core/frontdoor.server"; + serverToClientServiceProperties, +} from "~/logic/config.server"; export async function loader({ request }: LoaderFunctionArgs) { // This is the only place where we can read this field. @@ -76,7 +80,17 @@ export default function App() { return ( <GlobalPropertiesContext.Provider value={loaderData.globalProperties}> <ServicePropertiesContext.Provider value={loaderData.serviceProperties}> - <Outlet /> + <FrontDoorInfoContext.Provider + value={loaderData.serviceProperties.frontDoorInfo} + > + <ServiceLinksContext.Provider value={loaderData.serviceProperties}> + <OverdueThresholdsContext.Provider + value={loaderData.serviceProperties} + > + <Outlet /> + </OverdueThresholdsContext.Provider> + </ServiceLinksContext.Provider> + </FrontDoorInfoContext.Provider> </ServicePropertiesContext.Provider> </GlobalPropertiesContext.Provider> ); diff --git a/src/webui/app/routes/app/index.tsx b/src/webui/app/routes/app/index.tsx index 9b80a558b..ba661f066 100644 --- a/src/webui/app/routes/app/index.tsx +++ b/src/webui/app/routes/app/index.tsx @@ -9,7 +9,8 @@ import { StandaloneContainer } from "@jupiter/core/infra/component/layout/standa import { SmartAppBar } from "@jupiter/core/infra/component/smart-appbar"; import { Logo } from "@jupiter/core/infra/component/logo"; import { Title } from "@jupiter/core/infra/component/title"; -import { ServicePropertiesContext } from "@jupiter/core/config-client"; + +import { ServicePropertiesContext } from "~/logic/config"; export const shouldRevalidate: ShouldRevalidateFunction = () => false; diff --git a/src/webui/app/routes/app/lifecycle/init/google/create-or-login-user.tsx b/src/webui/app/routes/app/lifecycle/init/google/create-or-login-user.tsx index 40f2310c9..e145de1e5 100644 --- a/src/webui/app/routes/app/lifecycle/init/google/create-or-login-user.tsx +++ b/src/webui/app/routes/app/lifecycle/init/google/create-or-login-user.tsx @@ -9,13 +9,11 @@ import { loadGoogleOauthState, } from "@jupiter/core/auth/sub/google/oauth-state.server"; import { AUTH_TOKEN_NAME } from "@jupiter/core/infra/names"; -import { - GLOBAL_PROPERTIES, - SERVICE_PROPERTIES, -} from "@jupiter/core/config-server"; +import { GLOBAL_PROPERTIES } from "@jupiter/core/config-server"; import { isLocal } from "@jupiter/core/env"; import { getGuestApiClient } from "~/api-clients.server"; +import { SERVICE_PROPERTIES } from "~/logic/config.server"; import { emailVerificationVerifyUrl } from "~/routes/app/lifecycle/lifecycle-redirects.server"; import { commitSession, getSession } from "~/sessions"; @@ -48,7 +46,11 @@ export async function loader({ request }: LoaderFunctionArgs) { if (query.error !== undefined) { return redirect("/app/lifecycle/login/local/login", { - headers: { "Set-Cookie": await clearGoogleOauthState() }, + headers: { + "Set-Cookie": await clearGoogleOauthState( + SERVICE_PROPERTIES.sessionCookieSecure, + ), + }, }); } @@ -72,7 +74,10 @@ export async function loader({ request }: LoaderFunctionArgs) { const headers = new Headers(); headers.append("Set-Cookie", await commitSession(session)); - headers.append("Set-Cookie", await clearGoogleOauthState()); + headers.append( + "Set-Cookie", + await clearGoogleOauthState(SERVICE_PROPERTIES.sessionCookieSecure), + ); return redirect(emailVerificationVerifyUrl(result.new_user.ref_id), { headers, @@ -80,7 +85,11 @@ export async function loader({ request }: LoaderFunctionArgs) { } catch (error) { if (error instanceof ApiError && error.status === StatusCodes.CONFLICT) { return redirect("/app/lifecycle/util/user-already-exists", { - headers: { "Set-Cookie": await clearGoogleOauthState() }, + headers: { + "Set-Cookie": await clearGoogleOauthState( + SERVICE_PROPERTIES.sessionCookieSecure, + ), + }, }); } diff --git a/src/webui/app/routes/app/lifecycle/init/google/prepare.tsx b/src/webui/app/routes/app/lifecycle/init/google/prepare.tsx index a4c759ba7..f461b0e80 100644 --- a/src/webui/app/routes/app/lifecycle/init/google/prepare.tsx +++ b/src/webui/app/routes/app/lifecycle/init/google/prepare.tsx @@ -1,13 +1,11 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; import { redirect } from "@remix-run/node"; import { saveGoogleOauthState } from "@jupiter/core/auth/sub/google/oauth-state.server"; -import { - GLOBAL_PROPERTIES, - SERVICE_PROPERTIES, -} from "@jupiter/core/config-server"; +import { GLOBAL_PROPERTIES } from "@jupiter/core/config-server"; import { isLocal } from "@jupiter/core/env"; import { getGuestApiClient } from "~/api-clients.server"; +import { SERVICE_PROPERTIES } from "~/logic/config.server"; const GOOGLE_INIT_CALLBACK_PATH = "/app/lifecycle/init/google/create-or-login-user"; @@ -63,7 +61,10 @@ export async function loader({ request }: LoaderFunctionArgs) { return redirect(result.authorisation_url, { headers: { - "Set-Cookie": await saveGoogleOauthState(result.state), + "Set-Cookie": await saveGoogleOauthState( + result.state, + SERVICE_PROPERTIES.sessionCookieSecure, + ), }, }); } diff --git a/src/webui/app/routes/app/lifecycle/init/google/ready.tsx b/src/webui/app/routes/app/lifecycle/init/google/ready.tsx index 4191d1493..40488dbe2 100644 --- a/src/webui/app/routes/app/lifecycle/init/google/ready.tsx +++ b/src/webui/app/routes/app/lifecycle/init/google/ready.tsx @@ -8,6 +8,8 @@ import { isAllowedGoogleOauthCallbackUrl, } from "@jupiter/core/auth/sub/google/google-oauth-redirect-state.server"; +import { SERVICE_PROPERTIES } from "~/logic/config.server"; + const QuerySchema = z.object({ state: z.string(), code: z.string().optional(), @@ -26,8 +28,14 @@ export async function loader({ request }: LoaderFunctionArgs) { } if ( - !isAllowedGoogleOauthCallbackUrl(decoded.callbackSuccessUrl) || - !isAllowedGoogleOauthCallbackUrl(decoded.callbackFailureUrl) + !isAllowedGoogleOauthCallbackUrl( + decoded.callbackSuccessUrl, + SERVICE_PROPERTIES.webUiUrl, + ) || + !isAllowedGoogleOauthCallbackUrl( + decoded.callbackFailureUrl, + SERVICE_PROPERTIES.webUiUrl, + ) ) { throw new Response(ReasonPhrases.UNAUTHORIZED, { status: StatusCodes.UNAUTHORIZED, diff --git a/src/webui/app/routes/app/lifecycle/init/local/create-user.tsx b/src/webui/app/routes/app/lifecycle/init/local/create-user.tsx index e1ded0fab..d43f9fa96 100644 --- a/src/webui/app/routes/app/lifecycle/init/local/create-user.tsx +++ b/src/webui/app/routes/app/lifecycle/init/local/create-user.tsx @@ -18,7 +18,6 @@ import { Logo } from "@jupiter/core/infra/component/logo"; import { Password } from "@jupiter/core/auth/component/password"; import { LifecycleOAuthProviderButtons } from "@jupiter/core/auth/component/lifecycle-oauth-provider-buttons"; import { Title } from "@jupiter/core/infra/component/title"; -import { ServicePropertiesContext } from "@jupiter/core/config-client"; import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { ActionsExpansion, @@ -34,6 +33,7 @@ import { import { EMPTY_CONTEXT } from "@jupiter/core/infra/top-level-context"; import { AUTH_TOKEN_NAME } from "@jupiter/core/infra/names"; +import { ServicePropertiesContext } from "~/logic/config"; import { getGuestApiClient } from "~/api-clients.server"; import { emailVerificationVerifyUrl, diff --git a/src/webui/app/routes/app/lifecycle/login/local/login.tsx b/src/webui/app/routes/app/lifecycle/login/local/login.tsx index 6f6db93f0..be894a8e0 100644 --- a/src/webui/app/routes/app/lifecycle/login/local/login.tsx +++ b/src/webui/app/routes/app/lifecycle/login/local/login.tsx @@ -18,7 +18,6 @@ import { Logo } from "@jupiter/core/infra/component/logo"; import { Password } from "@jupiter/core/auth/component/password"; import { LifecycleOAuthProviderButtons } from "@jupiter/core/auth/component/lifecycle-oauth-provider-buttons"; import { Title } from "@jupiter/core/infra/component/title"; -import { ServicePropertiesContext } from "@jupiter/core/config-client"; import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { AUTH_TOKEN_NAME } from "@jupiter/core/infra/names"; import { @@ -34,6 +33,7 @@ import { } from "@jupiter/core/infra/component/section-actions"; import { EMPTY_CONTEXT } from "@jupiter/core/infra/top-level-context"; +import { ServicePropertiesContext } from "~/logic/config"; import { commitSession, getSession } from "~/sessions"; import { getGuestApiClient } from "~/api-clients.server"; diff --git a/src/webui/app/routes/app/lifecycle/logout.server.ts b/src/webui/app/routes/app/lifecycle/logout.server.ts index ba63fad1a..90654ab9e 100644 --- a/src/webui/app/routes/app/lifecycle/logout.server.ts +++ b/src/webui/app/routes/app/lifecycle/logout.server.ts @@ -2,6 +2,7 @@ import { redirectDocument } from "@remix-run/node"; import { clearGoogleOauthState } from "@jupiter/core/auth/sub/google/oauth-state.server"; import { AUTH_TOKEN_NAME } from "@jupiter/core/infra/names"; +import { SERVICE_PROPERTIES } from "~/logic/config.server"; import { destroySession, getSession } from "~/sessions"; const LOGIN_URL = "/app/lifecycle/login/local/login"; @@ -12,7 +13,10 @@ export async function logoutAndRedirectToLogin(request: Request) { const headers = new Headers(); headers.append("Set-Cookie", await destroySession(session)); - headers.append("Set-Cookie", await clearGoogleOauthState()); + headers.append( + "Set-Cookie", + await clearGoogleOauthState(SERVICE_PROPERTIES.sessionCookieSecure), + ); return redirectDocument(LOGIN_URL, { headers }); } diff --git a/src/webui/app/routes/app/workspace/calendar/schedule/export/$id.tsx b/src/webui/app/routes/app/workspace/calendar/schedule/export/$id.tsx index a36135ee6..a9978644c 100644 --- a/src/webui/app/routes/app/workspace/calendar/schedule/export/$id.tsx +++ b/src/webui/app/routes/app/workspace/calendar/schedule/export/$id.tsx @@ -35,13 +35,13 @@ import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { useBigScreen } from "@jupiter/core/infra/component/use-big-screen"; import { entityLinkStd } from "@jupiter/core/common/entity-link"; import { TagsEditor } from "@jupiter/core/common/sub/tags/component/tags-editor"; -import { ServicePropertiesContext } from "@jupiter/core/config-client"; import { noteStdOwner } from "#/core/common/sub/notes/note-std-owner"; import { selectZod, fixSelectOutputEntityId, } from "@jupiter/core/common/select-form"; +import { useServiceProperties } from "~/logic/config"; import { basicShouldRevalidate } from "~/rendering/standard-should-revalidate"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { getLoggedInApiClient } from "~/api-clients.server"; @@ -191,7 +191,7 @@ export default function ScheduleExportViewOne() { const [query] = useSearchParams(); const isBigScreen = useBigScreen(); const [hasCopiedExternalUrl, setHasCopiedExternalUrl] = useState(false); - const serviceProperties = useContext(ServicePropertiesContext); + const serviceProperties = useServiceProperties(); const inputsEnabled = navigation.state === "idle" && !loaderData.scheduleExport.archived; diff --git a/src/webui/app/routes/app/workspace/core/inbox-tasks.tsx b/src/webui/app/routes/app/workspace/core/inbox-tasks.tsx index 30416e023..7f8a1299e 100644 --- a/src/webui/app/routes/app/workspace/core/inbox-tasks.tsx +++ b/src/webui/app/routes/app/workspace/core/inbox-tasks.tsx @@ -70,7 +70,6 @@ import { } from "@jupiter/core/infra/component/section-actions"; import { StandardDivider } from "@jupiter/core/infra/component/standard-divider"; import { TabPanel } from "@jupiter/core/infra/component/tab-panel"; -import { ServicePropertiesContext } from "@jupiter/core/config-client"; import type { SomeErrorNoData } from "@jupiter/core/infra/action-result"; import { ActionableTime, @@ -84,6 +83,7 @@ import { import type { TopLevelInfo } from "@jupiter/core/infra/top-level-context"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; +import { useServiceProperties } from "~/logic/config"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; import { getLoggedInApiClient } from "~/api-clients.server"; @@ -123,7 +123,7 @@ export default function InboxTasks() { const topLevelInfo = useContext(TopLevelInfoContext); const { entries } = useLoaderDataSafeForAnimation<typeof loader>(); - const serviceProperties = useContext(ServicePropertiesContext); + const serviceProperties = useServiceProperties(); const isBigScreen = useBigScreen(); const shouldShowALeaf = useTrunkNeedsToShowLeaf(); diff --git a/src/webui/app/routes/app/workspace/core/publish.tsx b/src/webui/app/routes/app/workspace/core/publish.tsx index bcce9d805..74eea001c 100644 --- a/src/webui/app/routes/app/workspace/core/publish.tsx +++ b/src/webui/app/routes/app/workspace/core/publish.tsx @@ -33,8 +33,9 @@ import { import { SlimChip } from "@jupiter/core/infra/component/chips"; import { PublishOwnerTypeChip } from "#/core/common/sub/publish/components/publish-owner-type-chip"; import { publishOwnerEntityTagName } from "#/core/common/sub/publish/publish-owner-type-name"; -import { ServicePropertiesContext } from "#/core/config-client"; +import { publishedShareUrl } from "#/core/common/sub/publish/published-share-url"; +import { ServicePropertiesContext } from "~/logic/config"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; import { getLoggedInApiClient } from "~/api-clients.server"; @@ -140,7 +141,10 @@ export default function PublishEntities() { <EntityStack> {filteredPublishEntities.map((publishEntity) => { - const publicUrl = `${serviceProperties.webUiUrl}/app/public/published/${publishEntity.external_id}`; + const publicUrl = publishedShareUrl( + serviceProperties.publishedUrl, + publishEntity.external_id, + ); const isActive = publishEntity.status === PublishEntityStatus.ACTIVE; diff --git a/src/webui/app/routes/app/workspace/manage-api.tsx b/src/webui/app/routes/app/workspace/manage-api.tsx index f8e667469..dc79267a7 100644 --- a/src/webui/app/routes/app/workspace/manage-api.tsx +++ b/src/webui/app/routes/app/workspace/manage-api.tsx @@ -13,7 +13,6 @@ import { useTrunkNeedsToShowLeaf, DisplayType, } from "@jupiter/core/infra/component/use-nested-entities"; -import { ServicePropertiesContext } from "@jupiter/core/config-client"; import { TopLevelInfoContext } from "@jupiter/core/infra/top-level-context"; import { NavSingle, @@ -31,6 +30,7 @@ import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import { DocsHelp } from "#/core/infra/component/docs-help"; import { DocsHelpSubject } from "@jupiter/webapi-client"; +import { useServiceProperties } from "~/logic/config"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; import { getLoggedInApiClient } from "~/api-clients.server"; @@ -55,7 +55,7 @@ export const shouldRevalidate: ShouldRevalidateFunction = export default function ManageApi() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); - const serviceProperties = useContext(ServicePropertiesContext); + const serviceProperties = useServiceProperties(); const topLevelInfo = useContext(TopLevelInfoContext); const shouldShowALeaf = useTrunkNeedsToShowLeaf(); diff --git a/src/webui/app/routes/app/workspace/manage-mcp.tsx b/src/webui/app/routes/app/workspace/manage-mcp.tsx index cd5be5973..da020ccda 100644 --- a/src/webui/app/routes/app/workspace/manage-mcp.tsx +++ b/src/webui/app/routes/app/workspace/manage-mcp.tsx @@ -27,9 +27,9 @@ import { EntityStack } from "@jupiter/core/infra/component/entity-stack"; import { McpKeyView } from "@jupiter/core/mcp_key/components/mcp-key-view"; import { Stack, Typography } from "@mui/material"; import { DocsHelp } from "#/core/infra/component/docs-help"; -import { ServicePropertiesContext } from "#/core/config-client"; import { DocsHelpSubject } from "@jupiter/webapi-client"; +import { useServiceProperties } from "~/logic/config"; import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate"; import { getLoggedInApiClient } from "~/api-clients.server"; @@ -55,7 +55,7 @@ export const shouldRevalidate: ShouldRevalidateFunction = export default function ManageMcp() { const loaderData = useLoaderDataSafeForAnimation<typeof loader>(); const topLevelInfo = useContext(TopLevelInfoContext); - const serviceProperties = useContext(ServicePropertiesContext); + const serviceProperties = useServiceProperties(); const shouldShowALeaf = useTrunkNeedsToShowLeaf(); const shouldShowALeaflet = useLeafNeedsToShowLeaflet(); diff --git a/src/webui/app/routes/app/workspace/working-mem.tsx b/src/webui/app/routes/app/workspace/working-mem.tsx index 7e4d01627..a08722154 100644 --- a/src/webui/app/routes/app/workspace/working-mem.tsx +++ b/src/webui/app/routes/app/workspace/working-mem.tsx @@ -1,10 +1,15 @@ +import { ApiError, NamedEntityTag } from "@jupiter/webapi-client"; import TuneIcon from "@mui/icons-material/Tune"; -import type { LoaderFunctionArgs } from "@remix-run/node"; -import { json } from "@remix-run/node"; +import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; +import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; -import { Outlet, useNavigation } from "@remix-run/react"; +import { Form, Outlet, useNavigation } from "@remix-run/react"; import { AnimatePresence } from "framer-motion"; +import { StatusCodes } from "http-status-codes"; import { useContext } from "react"; +import { z } from "zod"; +import { parseForm } from "zodix"; +import { PublishPanel } from "@jupiter/core/common/sub/publish/components/publish-panel"; import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; import { makeTrunkErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; import { NestingAwareBlock } from "@jupiter/core/infra/component/layout/nesting-aware-block"; @@ -15,6 +20,7 @@ import { NavSingle, } from "@jupiter/core/infra/component/section-actions"; import { SectionCard } from "@jupiter/core/infra/component/section-card"; +import { validationErrorToUIErrorInfo } from "@jupiter/core/infra/action-result"; import { DisplayType, useTrunkNeedsToShowBranch, @@ -26,6 +32,21 @@ import { standardShouldRevalidate } from "~/rendering/standard-should-revalidate import { useLoaderDataSafeForAnimation } from "~/rendering/use-loader-data-for-animation"; import { getLoggedInApiClient } from "~/api-clients.server"; +const UpdateFormSchema = z.discriminatedUnion("intent", [ + z.object({ + intent: z.literal("create-publish"), + publishOwner: z.string(), + }), + z.object({ + intent: z.literal("activate-publish"), + publishEntityRefId: z.string(), + }), + z.object({ + intent: z.literal("to-draft-publish"), + publishEntityRefId: z.string(), + }), +]); + export const handle = { displayType: DisplayType.TRUNK, }; @@ -38,6 +59,51 @@ export async function loader({ request }: LoaderFunctionArgs) { }); } +export async function action({ request }: ActionFunctionArgs) { + const apiClient = await getLoggedInApiClient(request); + const form = await parseForm(request, UpdateFormSchema); + + try { + switch (form.intent) { + case "create-publish": { + await apiClient.publish.publishEntityCreate({ + owner: form.publishOwner, + }); + + return redirect("/app/workspace/working-mem"); + } + + case "activate-publish": { + await apiClient.publish.publishEntityActivate({ + ref_id: form.publishEntityRefId, + }); + + return redirect("/app/workspace/working-mem"); + } + + case "to-draft-publish": { + await apiClient.publish.publishEntityToDraft({ + ref_id: form.publishEntityRefId, + }); + + return redirect("/app/workspace/working-mem"); + } + + default: + throw new Response("Bad Intent", { status: 500 }); + } + } catch (error) { + if ( + error instanceof ApiError && + error.status === StatusCodes.UNPROCESSABLE_ENTITY + ) { + return json(validationErrorToUIErrorInfo(error.body)); + } + + throw error; + } +} + export const shouldRevalidate: ShouldRevalidateFunction = standardShouldRevalidate; @@ -73,6 +139,16 @@ export default function WorkingMem() { shouldHide={shouldShowABranch || shouldShowALeafToo} > <ToolPanel> + <Form method="post"> + <PublishPanel + entityType={NamedEntityTag.WORKING_MEM} + entityRefId={loaderData.entry.working_mem.ref_id} + topLevelInfo={topLevelInfo} + inputsEnabled={inputsEnabled} + publishEntity={loaderData.entry.publish_entity ?? null} + /> + </Form> + <SectionCard title="Working Mem"> <EntityNoteEditor initialNote={loaderData.entry.note} diff --git a/src/webui/app/routes/pwa-manifest.tsx b/src/webui/app/routes/pwa-manifest.tsx index 0079818d4..05d3e7779 100644 --- a/src/webui/app/routes/pwa-manifest.tsx +++ b/src/webui/app/routes/pwa-manifest.tsx @@ -1,12 +1,11 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; import { json } from "@remix-run/node"; -import { - GLOBAL_PROPERTIES, - SERVICE_PROPERTIES, -} from "@jupiter/core/config-server"; +import { GLOBAL_PROPERTIES } from "@jupiter/core/config-server"; import { inferPlatformAndDistribution } from "@jupiter/core/frontdoor.server"; import { getPublicName } from "#/core/utils"; +import { SERVICE_PROPERTIES } from "~/logic/config.server"; + export async function loader({ request }: LoaderFunctionArgs) { const name = getPublicName(GLOBAL_PROPERTIES); diff --git a/src/webui/app/routes/test-manifest.tsx b/src/webui/app/routes/test-manifest.tsx index 33755b380..fc3d021f6 100644 --- a/src/webui/app/routes/test-manifest.tsx +++ b/src/webui/app/routes/test-manifest.tsx @@ -1,10 +1,9 @@ import { json } from "@remix-run/node"; -import { - GLOBAL_PROPERTIES, - SERVICE_PROPERTIES, -} from "@jupiter/core/config-server"; +import { GLOBAL_PROPERTIES } from "@jupiter/core/config-server"; import { isDevelopment } from "#/core/env"; +import { SERVICE_PROPERTIES } from "~/logic/config.server"; + export async function loader() { if (!isDevelopment(GLOBAL_PROPERTIES.env)) { throw new Response(null, { status: 404 }); @@ -15,6 +14,7 @@ export async function loader() { apiUrl: SERVICE_PROPERTIES.apiUrl, mcpUrl: SERVICE_PROPERTIES.mcpUrl, webUiUrl: SERVICE_PROPERTIES.webUiUrl, + publishedUrl: SERVICE_PROPERTIES.publishedUrl, docsUrl: SERVICE_PROPERTIES.docsUrl, authStrategy: GLOBAL_PROPERTIES.authProvider, emailVerificationStrategy: GLOBAL_PROPERTIES.emailVerificationStrategy, diff --git a/src/webui/app/sessions.ts b/src/webui/app/sessions.ts index 65897cb89..47f8d7059 100644 --- a/src/webui/app/sessions.ts +++ b/src/webui/app/sessions.ts @@ -1,8 +1,9 @@ import type { AuthTokenExt } from "@jupiter/webapi-client"; import { createCookieSessionStorage } from "@remix-run/node"; -import { SERVICE_PROPERTIES } from "@jupiter/core/config-server"; import { SESSION_COOKIE_NAME } from "@jupiter/core/infra/names"; +import { SERVICE_PROPERTIES } from "~/logic/config.server"; + export class SessionInfoNotFoundError extends Error { constructor() { super("Session info not found"); diff --git a/tasks/_common.sh b/tasks/_common.sh index 4bedbe278..a9d377d00 100644 --- a/tasks/_common.sh +++ b/tasks/_common.sh @@ -17,6 +17,7 @@ export STANDARD_WEBAPI_POSTGRES_PORT=8005 export STANDARD_API_PORT=8020 export STANDARD_MCP_PORT=8030 export STANDARD_WEBUI_PORT=10020 +export STANDARD_PUBLISHED_PORT=10040 export STANDARD_DOCS_PORT=8000 # Local WebAPI Postgres sidecar defaults (pm2 docker run / dev tooling). @@ -331,10 +332,11 @@ run_jupiter_webapp() { local WEBAPI_POSTGRES_PORT=$4 local API_PORT=$5 local WEBUI_PORT=$6 - local DOCS_PORT=$7 - local MCP_PORT=$8 - local should_wait=$9 - shift 9 + local PUBLISHED_PORT=$7 + local DOCS_PORT=$8 + local MCP_PORT=$9 + local should_wait=${10} + shift 10 local should_monit=$1 local in_ci=$2 local source=$3 @@ -388,16 +390,16 @@ run_jupiter_webapp() { mkdir -p "$RUN_ROOT/$INSTANCE" - log info "Running Jupiter WebApi in universe: $UNIVERSE, instance: $INSTANCE, webapi port: $WEBAPI_PORT, webapi postgres port: $WEBAPI_POSTGRES_PORT, api port: $API_PORT, webui port: $WEBUI_PORT, docs port: $DOCS_PORT, mcp port: $MCP_PORT, webapi blend (ADR 0008) storage=$webapi_storage_engine telemetry=$webapi_telemetry search=$webapi_search crm=$webapi_crm auth_provider=$webapi_auth_provider email_verification_strategy=$email_verification_strategy email_sender=$webapi_email_sender, source: $source, version: $version, mode: $mode" + log info "Running Jupiter WebApi in universe: $UNIVERSE, instance: $INSTANCE, webapi port: $WEBAPI_PORT, webapi postgres port: $WEBAPI_POSTGRES_PORT, api port: $API_PORT, webui port: $WEBUI_PORT, published port: $PUBLISHED_PORT, docs port: $DOCS_PORT, mcp port: $MCP_PORT, webapi blend (ADR 0008) storage=$webapi_storage_engine telemetry=$webapi_telemetry search=$webapi_search crm=$webapi_crm auth_provider=$webapi_auth_provider email_verification_strategy=$email_verification_strategy email_sender=$webapi_email_sender, source: $source, version: $version, mode: $mode" if [[ "$UNIVERSE" == "dev" ]]; then if [[ "$mode" == "pm2" ]]; then - _run_dev_jupiter_webapp_with_pm2 "$INSTANCE" "$WEBAPI_PORT" "$WEBAPI_POSTGRES_PORT" "$API_PORT" "$WEBUI_PORT" "$DOCS_PORT" "$MCP_PORT" "$should_wait" "$should_monit" "$in_ci" "$source" "$version" "$clear_first" "$webapi_storage_engine" "$webapi_telemetry" "$webapi_search" "$webapi_crm" "$webapi_auth_provider" "$webapi_email_sender" "$email_verification_strategy" + _run_dev_jupiter_webapp_with_pm2 "$INSTANCE" "$WEBAPI_PORT" "$WEBAPI_POSTGRES_PORT" "$API_PORT" "$WEBUI_PORT" "$PUBLISHED_PORT" "$DOCS_PORT" "$MCP_PORT" "$should_wait" "$should_monit" "$in_ci" "$source" "$version" "$clear_first" "$webapi_storage_engine" "$webapi_telemetry" "$webapi_search" "$webapi_crm" "$webapi_auth_provider" "$webapi_email_sender" "$email_verification_strategy" else - _run_dev_jupiter_webapp_with_docker "$INSTANCE" "$WEBAPI_PORT" "$WEBAPI_POSTGRES_PORT" "$API_PORT" "$WEBUI_PORT" "$DOCS_PORT" "$MCP_PORT" "$should_wait" "$should_monit" "$in_ci" "$source" "$version" "$clear_first" "$webapi_storage_engine" "$webapi_telemetry" "$webapi_search" "$webapi_crm" "$webapi_auth_provider" "$webapi_email_sender" "$email_verification_strategy" + _run_dev_jupiter_webapp_with_docker "$INSTANCE" "$WEBAPI_PORT" "$WEBAPI_POSTGRES_PORT" "$API_PORT" "$WEBUI_PORT" "$PUBLISHED_PORT" "$DOCS_PORT" "$MCP_PORT" "$should_wait" "$should_monit" "$in_ci" "$source" "$version" "$clear_first" "$webapi_storage_engine" "$webapi_telemetry" "$webapi_search" "$webapi_crm" "$webapi_auth_provider" "$webapi_email_sender" "$email_verification_strategy" fi elif [[ "$UNIVERSE" == "thrive-sh-test" ]]; then - _run_thrive_sh_test_webapp "$INSTANCE" "$WEBAPI_PORT" "$WEBAPI_POSTGRES_PORT" "$API_PORT" "$WEBUI_PORT" "$DOCS_PORT" "$MCP_PORT" "$should_wait" "$should_monit" "$in_ci" "$source" "$version" "$clear_first" "$webapi_storage_engine" "$webapi_telemetry" "$webapi_search" "$webapi_crm" "$webapi_auth_provider" "$webapi_email_sender" "$email_verification_strategy" + _run_thrive_sh_test_webapp "$INSTANCE" "$WEBAPI_PORT" "$WEBAPI_POSTGRES_PORT" "$API_PORT" "$WEBUI_PORT" "$PUBLISHED_PORT" "$DOCS_PORT" "$MCP_PORT" "$should_wait" "$should_monit" "$in_ci" "$source" "$version" "$clear_first" "$webapi_storage_engine" "$webapi_telemetry" "$webapi_search" "$webapi_crm" "$webapi_auth_provider" "$webapi_email_sender" "$email_verification_strategy" else log error "Unknown universe: $UNIVERSE" exit 1 @@ -424,18 +426,22 @@ _run_dev_jupiter_webapp_with_pm2() { local apiLogFile=../../$RUN_ROOT/$instance/api.log local webuiPort=$5 local webuiServerUrl=http://localhost:${webuiPort} + local publishedLogFile=../../$RUN_ROOT/$instance/published.log + local publishedPort=$6 + local publishedServerUrl=http://localhost:${publishedPort} local docsLogFile=../../$RUN_ROOT/$instance/docs.log - local docsPort=$6 + local docsPort=$7 local docsServerUrl=http://localhost:${docsPort} local docsPublicName=$PUBLIC_NAME local docsAuthor=$AUTHOR local docsCopyright=$COPYRIGHT - local mcpPort=$7 + local mcpPort=$8 local mcpServerUrl=http://localhost:${mcpPort} local mcpLogFile=../../$RUN_ROOT/$instance/mcp.log - local should_wait=$8 - local should_monit=$9 + local should_wait=$9 shift 9 + local should_monit=$1 + shift 1 local in_ci=$1 local source=$2 local version=$3 @@ -491,7 +497,7 @@ _run_dev_jupiter_webapp_with_pm2() { write_jupiter_run_webapi_env "$instance" "$webapi_storage_engine" "$DEV_POSTGRES_HOST" "$webapiPostgresPort" "$webapiPostgresUser" "$webapiPostgresPassword" "$webapiPostgresDb" - pm2_base_data=$(jo instance="$instance" webapiLogFile="$webapiLogFile" webapiSqliteDbUrl="$webapiSqliteDbUrl" webapiPort="$webapiPort" webapiServerUrl="$webapiServerUrl" webapiPostgresLogFile="$webapiPostgresLogFile" webapiPostgresPort="$webapiPostgresPort" webapiPostgresDb="$webapiPostgresDb" webapiPostgresUser="$webapiPostgresUser" webapiPostgresPassword="$webapiPostgresPassword" webapiPostgresPgdataHostPath="$webapiPostgresPgdataHostPath" webapiPostgresVersion="$POSTGRES_VERSION" webapiStorageEngine="$webapi_storage_engine" jupiterTelemetry="$webapi_telemetry" webapiSearch="$webapi_search" jupiterCrm="$webapi_crm" jupiterAuthProvider="$webapi_auth_provider" jupiterEmailVerificationStrategy="$email_verification_strategy" jupiterEmailSender="$webapi_email_sender" webapiPostgresDbUrl="$webapiPostgresDbUrl" webapiAlembicIniPath="$webapiAlembicIniPath" webapiAlembicMigrationsPath="$webapiAlembicMigrationsPath" webapiSqliteOnly=$webapiSqliteOnly webapiCronExecutionMode="$WEBAPI_CRON_EXECUTION_MODE_LOCAL" apiLogFile="$apiLogFile" apiPort="$apiPort" apiServerUrl="$apiServerUrl" webuiLogFile="$webuiLogFile" webuiPort="$webuiPort" webuiServerUrl="$webuiServerUrl" docsLogFile="$docsLogFile" docsPort="$docsPort" docsServerUrl="$docsServerUrl" docsPublicName="$docsPublicName" docsAuthor="$docsAuthor" docsCopyright="$docsCopyright" mcpLogFile="$mcpLogFile" mcpPort="$mcpPort" mcpServerUrl="$mcpServerUrl") + pm2_base_data=$(jo instance="$instance" webapiLogFile="$webapiLogFile" webapiSqliteDbUrl="$webapiSqliteDbUrl" webapiPort="$webapiPort" webapiServerUrl="$webapiServerUrl" webapiPostgresLogFile="$webapiPostgresLogFile" webapiPostgresPort="$webapiPostgresPort" webapiPostgresDb="$webapiPostgresDb" webapiPostgresUser="$webapiPostgresUser" webapiPostgresPassword="$webapiPostgresPassword" webapiPostgresPgdataHostPath="$webapiPostgresPgdataHostPath" webapiPostgresVersion="$POSTGRES_VERSION" webapiStorageEngine="$webapi_storage_engine" jupiterTelemetry="$webapi_telemetry" webapiSearch="$webapi_search" jupiterCrm="$webapi_crm" jupiterAuthProvider="$webapi_auth_provider" jupiterEmailVerificationStrategy="$email_verification_strategy" jupiterEmailSender="$webapi_email_sender" webapiPostgresDbUrl="$webapiPostgresDbUrl" webapiAlembicIniPath="$webapiAlembicIniPath" webapiAlembicMigrationsPath="$webapiAlembicMigrationsPath" webapiSqliteOnly=$webapiSqliteOnly webapiCronExecutionMode="$WEBAPI_CRON_EXECUTION_MODE_LOCAL" apiLogFile="$apiLogFile" apiPort="$apiPort" apiServerUrl="$apiServerUrl" webuiLogFile="$webuiLogFile" webuiPort="$webuiPort" webuiServerUrl="$webuiServerUrl" publishedLogFile="$publishedLogFile" publishedPort="$publishedPort" publishedServerUrl="$publishedServerUrl" docsLogFile="$docsLogFile" docsPort="$docsPort" docsServerUrl="$docsServerUrl" docsPublicName="$docsPublicName" docsAuthor="$docsAuthor" docsCopyright="$docsCopyright" mcpLogFile="$mcpLogFile" mcpPort="$mcpPort" mcpServerUrl="$mcpServerUrl") data=$(jupiter_pm2_render_data_with_cron_apps "$pm2_base_data" "$instance" "$RUN_ROOT") if [[ "$in_ci" == "dev" ]]; then node tasks/_resources/render-hbs.mjs tasks/_resources/pm2.config.dev.js.hbs "$data" > "$RUN_ROOT/$instance/pm2.config.js" @@ -508,6 +514,7 @@ _run_dev_jupiter_webapp_with_pm2() { save_jupiter_url "$instance" "webapi:postgres" "$webapiPostgresServerUrl" save_jupiter_url "$instance" "api" "$apiServerUrl" save_jupiter_url "$instance" "webui" "$webuiServerUrl" + save_jupiter_url "$instance" "published" "$publishedServerUrl" save_jupiter_url "$instance" "docs" "$docsServerUrl" save_jupiter_url "$instance" "mcp" "$mcpServerUrl" @@ -515,6 +522,7 @@ _run_dev_jupiter_webapp_with_pm2() { wait_for_service_to_start webapi:srv "$webapiServerUrl" wait_for_service_to_start api "$apiServerUrl" wait_for_service_to_start webui "$webuiServerUrl" + wait_for_service_to_start published "$publishedServerUrl" # Skip docs in wait:all — MkDocs is slow; matrix/itest callers do not need it up first. wait_for_service_to_start mcp "$mcpServerUrl" fi @@ -531,6 +539,10 @@ _run_dev_jupiter_webapp_with_pm2() { wait_for_service_to_start webui "$webuiServerUrl" fi + if [[ ${should_wait} == "wait:published" ]]; then + wait_for_service_to_start published "$publishedServerUrl" + fi + if [[ ${should_wait} == "wait:docs" ]]; then wait_for_service_to_start docs "$docsServerUrl" fi @@ -569,15 +581,16 @@ _run_dev_jupiter_webapp_with_docker() { export API_SERVER_URL=http://localhost:${API_PORT} export WEBUI_PORT=$5 export WEBUI_SERVER_URL=https://localhost:${WEBUI_PORT} - export DOCS_PORT=$6 + export PUBLISHED_PORT=$6 + export PUBLISHED_SERVER_URL=https://localhost:${PUBLISHED_PORT} + export DOCS_PORT=$7 export DOCS_SERVER_URL=http://localhost:${DOCS_PORT} export PUBLIC_NAME export DOCS_AUTHOR=$AUTHOR export DOCS_COPYRIGHT=$COPYRIGHT - export MCP_PORT=$7 + export MCP_PORT=$8 export MCP_SERVER_URL=http://localhost:${MCP_PORT} - local should_wait=$8 - local should_monit=$9 + local should_wait=$9 shift 9 local in_ci=$1 local source=$2 @@ -638,6 +651,8 @@ _run_dev_jupiter_webapp_with_docker() { DOCKER_IMAGE_API=$(get_jupiter_image "api" "$source" "$version" arm64) export DOCKER_IMAGE_WEBUI DOCKER_IMAGE_WEBUI=$(get_jupiter_image "webui" "$source" "$version" arm64) + export DOCKER_IMAGE_PUBLISHED + DOCKER_IMAGE_PUBLISHED=$(get_jupiter_image "published" "$source" "$version" arm64) export DOCKER_IMAGE_DOCS DOCKER_IMAGE_DOCS=$(get_jupiter_image "docs" "$source" "$version" arm64) export DOCKER_IMAGE_MCP @@ -657,7 +672,7 @@ _run_dev_jupiter_webapp_with_docker() { write_jupiter_run_webapi_env "$instance" "$webapi_storage_engine" "$DEV_POSTGRES_HOST" "$WEBAPI_POSTGRES_PORT" "$DEV_POSTGRES_USER" "$DEV_POSTGRES_PASSWORD" "$DEV_POSTGRES_DB" - log info "Running docker images: $DOCKER_IMAGE_WEBAPI, $DOCKER_IMAGE_API, $DOCKER_IMAGE_WEBUI, $DOCKER_IMAGE_DOCS, $DOCKER_IMAGE_MCP, and ${#WEBAPI_CRON_FOLDERS[@]} webapi cron images" + log info "Running docker images: $DOCKER_IMAGE_WEBAPI, $DOCKER_IMAGE_API, $DOCKER_IMAGE_WEBUI, $DOCKER_IMAGE_PUBLISHED, $DOCKER_IMAGE_DOCS, $DOCKER_IMAGE_MCP, and ${#WEBAPI_CRON_FOLDERS[@]} webapi cron images" openssl req -x509 \ -nodes \ @@ -673,6 +688,7 @@ _run_dev_jupiter_webapp_with_docker() { save_jupiter_url "$instance" "webapi:postgres" "$WEBAPI_POSTGRES_SERVER_URL" save_jupiter_url "$instance" "api" "$API_SERVER_URL" save_jupiter_url "$instance" "webui" "$WEBUI_SERVER_URL" + save_jupiter_url "$instance" "published" "$PUBLISHED_SERVER_URL" save_jupiter_url "$instance" "docs" "$DOCS_SERVER_URL" save_jupiter_url "$instance" "mcp" "$MCP_SERVER_URL" @@ -684,6 +700,7 @@ _run_dev_jupiter_webapp_with_docker() { wait_for_service_to_start webapi:srv "$WEBAPI_SERVER_URL" wait_for_service_to_start api "$API_SERVER_URL" wait_for_service_to_start webui "$WEBUI_SERVER_URL" + wait_for_service_to_start published "$PUBLISHED_SERVER_URL" # Skip docs in wait:all — MkDocs is slow; matrix/itest callers do not need it up first. wait_for_service_to_start mcp "$MCP_SERVER_URL" fi @@ -700,6 +717,10 @@ _run_dev_jupiter_webapp_with_docker() { wait_for_service_to_start webui "$WEBUI_SERVER_URL" fi + if [[ ${should_wait} == "wait:published" ]]; then + wait_for_service_to_start published "$PUBLISHED_SERVER_URL" + fi + if [[ ${should_wait} == "wait:docs" ]]; then wait_for_service_to_start docs "$DOCS_SERVER_URL" fi @@ -762,6 +783,7 @@ _thrive_sh_test_default_docker_image_env_append_ssh() { echo "DOCKER_IMAGE_WEBAPI=jupiter/webapi-srv:${version}-${platform}" >> .env && echo "DOCKER_IMAGE_API=jupiter/api:${version}-${platform}" >> .env && echo "DOCKER_IMAGE_WEBUI=jupiter/webui:${version}-${platform}" >> .env && + echo "DOCKER_IMAGE_PUBLISHED=jupiter/published:${version}-${platform}" >> .env && echo "DOCKER_IMAGE_DOCS=jupiter/docs:${version}-${platform}" >> .env && echo "DOCKER_IMAGE_MCP=jupiter/mcp:${version}-${platform}" >> .env && EOF @@ -821,7 +843,7 @@ _thrive_sh_test_log_remote_env() { _thrive_sh_test_prepare_exit_cleanup() { local folder - rm -f webapi.tar api.tar webui.tar docs.tar mcp.tar 2>/dev/null || true + rm -f webapi.tar api.tar webui.tar published.tar docs.tar mcp.tar 2>/dev/null || true for folder in "${WEBAPI_CRON_FOLDERS[@]}"; do rm -f "$(jupiter_webapi_cron_tar_name "$folder")" 2>/dev/null || true done @@ -839,11 +861,13 @@ _run_thrive_sh_test_webapp() { local WEBAPI_POSTGRES_PORT=$3 local API_PORT=$4 local WEBUI_PORT=$5 - local DOCS_PORT=$6 - local MCP_PORT=$7 - local should_wait=$8 - local should_monit=$9 + local PUBLISHED_PORT=$6 + local DOCS_PORT=$7 + local MCP_PORT=$8 + local should_wait=$9 shift 9 + local should_monit=$1 + shift 1 local in_ci=$1 local source=$2 local version=$3 @@ -1068,6 +1092,7 @@ $(_thrive_sh_test_default_docker_image_env_append_ssh "$version" arm64 | sed 's/ docker save -o webapi.tar "jupiter/webapi-srv:${version}-arm64" docker save -o api.tar "jupiter/api:${version}-arm64" docker save -o webui.tar "jupiter/webui:${version}-arm64" + docker save -o published.tar "jupiter/published:${version}-arm64" docker save -o docs.tar "jupiter/docs:${version}-arm64" docker save -o mcp.tar "jupiter/mcp:${version}-arm64" for folder in "${WEBAPI_CRON_FOLDERS[@]}"; do @@ -1085,6 +1110,9 @@ $(_thrive_sh_test_default_docker_image_env_append_ssh "$version" arm64 | sed 's/ gcloud compute scp webui.tar "$gcp_vm_name":~/webui.tar \ --project "$THRIVE_GCP_PROJECT" \ --zone "$THRIVE_GCP_ZONE" + gcloud compute scp published.tar "$gcp_vm_name":~/published.tar \ + --project "$THRIVE_GCP_PROJECT" \ + --zone "$THRIVE_GCP_ZONE" gcloud compute scp docs.tar "$gcp_vm_name":~/docs.tar \ --project "$THRIVE_GCP_PROJECT" \ --zone "$THRIVE_GCP_ZONE" @@ -1110,6 +1138,7 @@ $(_thrive_sh_test_default_docker_image_env_append_ssh "$version" arm64 | sed 's/ sudo docker load -i webapi.tar && sudo docker load -i api.tar && sudo docker load -i webui.tar && + sudo docker load -i published.tar && sudo docker load -i docs.tar && sudo docker load -i mcp.tar && $(for folder in "${WEBAPI_CRON_FOLDERS[@]}"; do @@ -1138,7 +1167,10 @@ $(_thrive_sh_test_default_docker_image_env_append_ssh "$version" arm64 | sed 's/ echo \"DOCKER_IMAGE_WEBAPI=jupiter/webapi:${version}-arm64\" >> .env && echo \"DOCKER_IMAGE_API=jupiter/api:${version}-arm64\" >> .env && echo \"DOCKER_IMAGE_WEBUI=jupiter/webui:${version}-arm64\" >> .env && + echo \"DOCKER_IMAGE_PUBLISHED=jupiter/published:${version}-arm64\" >> .env && echo \"DOCKER_IMAGE_DOCS=jupiter/docs:${version}-arm64\" >> .env && + echo \"PUBLISHED_PORT=${PUBLISHED_TESTING_PORT}\" >> .env && + echo \"PUBLISHED_SERVER_URL=https://${gcp_dns_name}/publish\" >> .env && echo \"DOCKER_IMAGE_MCP=jupiter/mcp:${version}-arm64\" >> .env && $(for folder in "${WEBAPI_CRON_FOLDERS[@]}"; do cron_env_var=$(jupiter_webapi_cron_docker_env_var "$folder") @@ -1169,6 +1201,7 @@ $(_thrive_sh_test_default_docker_image_env_append_ssh "$version" arm64 | sed 's/ local thrive_host="${instance}${THRIVE_SH_TEST_DOMAIN}" local thrive_webui_url="https://${thrive_host}" + local thrive_published_url="http://${thrive_host}:${PUBLISHED_TESTING_PORT}" local thrive_webapi_url="http://${thrive_host}:${WEBAPI_TESTING_PORT}" local thrive_api_url="https://${thrive_host}/api" local thrive_docs_url="https://${thrive_host}/docs" @@ -1178,6 +1211,7 @@ $(_thrive_sh_test_default_docker_image_env_append_ssh "$version" arm64 | sed 's/ wait_for_service_to_start webapi:srv "$thrive_webapi_url" wait_for_service_to_start api "$thrive_api_url" wait_for_service_to_start webui "$thrive_webui_url" + wait_for_service_to_start published "$thrive_published_url" # Skip docs in wait:all — MkDocs is slow; matrix/itest callers do not need it up first. wait_for_service_to_start mcp "$thrive_mcp_url" fi @@ -1194,6 +1228,10 @@ $(_thrive_sh_test_default_docker_image_env_append_ssh "$version" arm64 | sed 's/ wait_for_service_to_start webui "$thrive_webui_url" fi + if [[ ${should_wait} == "wait:published" ]]; then + wait_for_service_to_start published "$thrive_published_url" + fi + if [[ ${should_wait} == "wait:docs" ]]; then wait_for_service_to_start docs "$thrive_docs_url" fi @@ -1284,6 +1322,63 @@ get_thrive_staging_webui_url() { echo "https://jupiter-webui-${instance}.${GLOBAL_HOSTED_INFRA_ROOT}" } +get_thrive_sh_test_published_url() { + local instance=$1 + echo "http://${instance}${THRIVE_SH_TEST_DOMAIN}:${PUBLISHED_TESTING_PORT}" +} + +get_thrive_production_published_url() { + echo "$HOSTED_GLOBAL_PUBLISHED_URL" +} + +get_thrive_staging_published_url() { + local instance=$1 + echo "https://jupiter-published-${instance}.${GLOBAL_HOSTED_INFRA_ROOT}" +} + +get_published_url_for_universe() { + local universe=$1 + local environment=$2 + local instance=$3 + + if [[ "$universe" == "dev" ]]; then + if [[ "$environment" != "local" ]]; then + log error "Environment $environment is not supported for dev universe" + exit 1 + fi + get_dev_service_url "$instance" "published" + return 0 + elif [[ "$universe" == "thrive-sh-test" ]]; then + if [[ "$environment" != "staging" ]]; then + log error "Environment $environment is not supported for thrive-sh-test universe" + exit 1 + fi + get_thrive_sh_test_published_url "$instance" + return 0 + elif [[ "$universe" == "thrive" ]]; then + if [[ "$environment" == "production" ]]; then + get_thrive_production_published_url + return 0 + elif [[ "$environment" == "staging" ]]; then + get_thrive_staging_published_url "$instance" + return 0 + else + log error "Environment $environment is not supported for thrive universe" + exit 1 + fi + elif [[ "$universe" =~ ^https?:// ]]; then + if [[ "$environment" != "production" ]]; then + log error "Environment $environment is not supported for custom universe" + exit 1 + fi + echo "$universe" + return 0 + else + log info "Unknown universe: $universe" + exit 1 + fi +} + get_webui_url_for_universe() { local universe=$1 local environment=$2 diff --git a/tasks/_resources/pm2.config.ci.js.hbs b/tasks/_resources/pm2.config.ci.js.hbs index a995bf9e1..b236d8239 100644 --- a/tasks/_resources/pm2.config.ci.js.hbs +++ b/tasks/_resources/pm2.config.ci.js.hbs @@ -122,10 +122,31 @@ module.exports = { API_URL: "{{apiServerUrl}}", MCP_URL: "{{mcpServerUrl}}", WEBUI_URL: "{{webuiServerUrl}}", + PUBLISHED_URL: "{{publishedServerUrl}}", DOCS_URL: "{{docsServerUrl}}", PORT: "{{webuiPort}}", } }, + { + name: "{{instance}}:published", + cwd: "src/published", + interpreter: "none", + script: "node_modules/.bin/remix", + args: "dev", + log_file: "{{publishedLogFile}}", + env: { + INSTANCE: "{{instance}}", + AUTH_PROVIDER: "{{jupiterAuthProvider}}", + EMAIL_VERIFICATION_STRATEGY: "{{jupiterEmailVerificationStrategy}}", + TELEMETRY: "{{jupiterTelemetry}}", + CRM: "{{jupiterCrm}}", + WEBAPI_SERVER_HOST: "0.0.0.0", + WEBAPI_SERVER_PORT: "{{webapiPort}}", + PUBLISHED_URL: "{{publishedServerUrl}}", + DOCS_URL: "{{docsServerUrl}}", + PORT: "{{publishedPort}}", + } + }, { name: "{{instance}}:docs", cwd: "src/docs", diff --git a/tasks/_resources/pm2.config.dev.js.hbs b/tasks/_resources/pm2.config.dev.js.hbs index eb7076659..4277a5c43 100644 --- a/tasks/_resources/pm2.config.dev.js.hbs +++ b/tasks/_resources/pm2.config.dev.js.hbs @@ -122,11 +122,33 @@ module.exports = { API_URL: "{{apiServerUrl}}", MCP_URL: "{{mcpServerUrl}}", WEBUI_URL: "{{webuiServerUrl}}", + PUBLISHED_URL: "{{publishedServerUrl}}", DOCS_URL: "{{docsServerUrl}}", PORT: "{{webuiPort}}", NODE_OPTIONS: "--trace-warnings" } }, + { + name: "{{instance}}:published", + cwd: "src/published", + interpreter: "none", + script: "node_modules/.bin/remix", + args: "dev", + log_file: "{{publishedLogFile}}", + env: { + INSTANCE: "{{instance}}", + AUTH_PROVIDER: "{{jupiterAuthProvider}}", + EMAIL_VERIFICATION_STRATEGY: "{{jupiterEmailVerificationStrategy}}", + TELEMETRY: "{{jupiterTelemetry}}", + CRM: "{{jupiterCrm}}", + WEBAPI_SERVER_HOST: "0.0.0.0", + WEBAPI_SERVER_PORT: "{{webapiPort}}", + PUBLISHED_URL: "{{publishedServerUrl}}", + DOCS_URL: "{{docsServerUrl}}", + PORT: "{{publishedPort}}", + NODE_OPTIONS: "--trace-warnings" + } + }, { name: "{{instance}}:docs", cwd: "src/docs", diff --git a/tasks/build/api-clients.sh b/tasks/build/api-clients.sh index af4e3426e..6ff79b33b 100755 --- a/tasks/build/api-clients.sh +++ b/tasks/build/api-clients.sh @@ -16,12 +16,13 @@ webapi_postgres_port=$(get_free_port) api_port=$(get_free_port) mcp_port=$(get_free_port) webui_port=$(get_free_port) +published_port=$(get_free_port) docs_port=$(get_free_port) api_url=http://0.0.0.0:${api_port} log info "Starting Jupiter for API client build with api port $api_port" -run_jupiter_webapp dev apigen "$webapi_port" "$webapi_postgres_port" "$api_port" "$webui_port" "$docs_port" "$mcp_port" wait:api no-monit ci local latest pm2 "" sqlite local sql noop +run_jupiter_webapp dev apigen "$webapi_port" "$webapi_postgres_port" "$api_port" "$webui_port" "$published_port" "$docs_port" "$mcp_port" wait:api no-monit ci local latest pm2 "" sqlite local sql noop log info "Extracting OpenAPI spec from API service" diff --git a/tasks/build/docker.sh b/tasks/build/docker.sh index c88f11ef6..9f34fc58a 100755 --- a/tasks/build/docker.sh +++ b/tasks/build/docker.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -#MISE description="Build Docker images for webapi, webapi crons, webui, api, mcp, docs, and cli" +#MISE description="Build Docker images for webapi, webapi crons, webui, published, api, mcp, docs, and cli" #USAGE flag "--log <log>" default="info" help="Log output" { #USAGE choices "info" "debug" "trace" #USAGE } @@ -77,6 +77,7 @@ image_specs=( "api:src/api/Dockerfile" "mcp:src/mcp/Dockerfile" "webui:src/webui/Dockerfile" + "published:src/published/Dockerfile" "docs:src/docs/Dockerfile" "cli:src/cli/Dockerfile" ) diff --git a/tasks/generate-client-code.sh b/tasks/generate-client-code.sh index 32db7c6fe..b4ef271ec 100755 --- a/tasks/generate-client-code.sh +++ b/tasks/generate-client-code.sh @@ -16,11 +16,12 @@ webapi_port=$(get_free_port) webapi_postgres_port=$(get_free_port) api_port=$(get_free_port) webui_port=$(get_free_port) +published_port=$(get_free_port) docs_port=$(get_free_port) mcp_port=$(get_free_port) webapi_url="http://0.0.0.0:${webapi_port}" -log info "Allocated ports: webapi=$webapi_port api=$api_port webui=$webui_port docs=$docs_port mcp=$mcp_port" +log info "Allocated ports: webapi=$webapi_port api=$api_port webui=$webui_port published=$published_port docs=$docs_port mcp=$mcp_port" # --- Cleanup trap (set once, covers everything) --- _cleanup() { @@ -37,7 +38,7 @@ trap _cleanup EXIT log info "Starting Jupiter for API generation on webapi port $webapi_port" run_jupiter_webapp \ dev apigen \ - "$webapi_port" "$webapi_postgres_port" "$api_port" "$webui_port" "$docs_port" "$mcp_port" \ + "$webapi_port" "$webapi_postgres_port" "$api_port" "$webui_port" "$published_port" "$docs_port" "$mcp_port" \ wait:webapi:srv no-monit ci local latest pm2 "" sqlite local sql noop # --- Extract OpenAPI spec --- diff --git a/tasks/release/upload-docker.sh b/tasks/release/upload-docker.sh index e3590d7d1..649d250ef 100755 --- a/tasks/release/upload-docker.sh +++ b/tasks/release/upload-docker.sh @@ -85,7 +85,7 @@ publish_image() { create_manifest "$name" "${VERSION}" } -for name in webapi-srv api mcp webui docs cli; do +for name in webapi-srv api mcp webui published docs cli; do publish_image "$name" done diff --git a/tasks/run/db/migrate.sh b/tasks/run/db/migrate.sh index 7a3d25ec2..18659f833 100755 --- a/tasks/run/db/migrate.sh +++ b/tasks/run/db/migrate.sh @@ -26,6 +26,7 @@ webapi_port=$(get_free_port) webapi_postgres_port=$(get_free_port) api_port=$(get_free_port) webui_port=$(get_free_port) +published_port=$(get_free_port) docs_port=$(get_free_port) mcp_port=$(get_free_port) @@ -40,7 +41,7 @@ fi # Run Jupiter with migrations - it will automatically run migrations on startup -run_jupiter_webapp dev "$instance" "$webapi_port" "$webapi_postgres_port" "$api_port" "$webui_port" "$docs_port" "$mcp_port" wait:webapi:srv no-monit ci local latest pm2 "" sqlite local sql noop +run_jupiter_webapp dev "$instance" "$webapi_port" "$webapi_postgres_port" "$api_port" "$webui_port" "$published_port" "$docs_port" "$mcp_port" wait:webapi:srv no-monit ci local latest pm2 "" sqlite local sql noop get_logs pm2 "$instance" webapi:srv diff --git a/tasks/run/instance/list.sh b/tasks/run/instance/list.sh index 00665cd13..5d44dd084 100755 --- a/tasks/run/instance/list.sh +++ b/tasks/run/instance/list.sh @@ -65,10 +65,11 @@ for instance in $instances; do webapi_url=$(get_dev_service_url "$instance" "webapi:srv") api_url=$(get_dev_service_url "$instance" "api") webui_url=$(get_dev_service_url "$instance" "webui") + published_url=$(get_dev_service_url "$instance" "published") docs_url=$(get_dev_service_url "$instance" "docs") db_file="$instance_path/jupiter.sqlite" - status_info="WebAPI: $webapi_url, API: $api_url, WebUI: $webui_url, Docs: $docs_url" + status_info="WebAPI: $webapi_url, API: $api_url, WebUI: $webui_url, Published: $published_url, Docs: $docs_url" if [[ -f "$db_file" ]]; then # Portable-ish size: parse ls -lh output (fifth field) diff --git a/tasks/run/srv.sh b/tasks/run/srv.sh index 0724b8efa..d4f858f67 100755 --- a/tasks/run/srv.sh +++ b/tasks/run/srv.sh @@ -64,6 +64,7 @@ if [[ -z "${usage_instance}" ]]; then api_port=$STANDARD_API_PORT mcp_port=$STANDARD_MCP_PORT webui_port=$STANDARD_WEBUI_PORT + published_port=$STANDARD_PUBLISHED_PORT docs_port=$STANDARD_DOCS_PORT elif [[ "${usage_instance}" == "+gen" ]]; then instance=$(get_instance) @@ -72,6 +73,7 @@ elif [[ "${usage_instance}" == "+gen" ]]; then api_port=$(get_free_port) mcp_port=$(get_free_port) webui_port=$(get_free_port) + published_port=$(get_free_port) docs_port=$(get_free_port) else instance="${usage_instance}" @@ -80,7 +82,8 @@ else api_port=$(get_free_port) mcp_port=$(get_free_port) webui_port=$(get_free_port) + published_port=$(get_free_port) docs_port=$(get_free_port) fi -run_jupiter_webapp "$usage_universe" "$instance" "$webapi_port" "$webapi_postgres_port" "$api_port" "$webui_port" "$docs_port" "$mcp_port" no-wait monit dev "$usage_source" "$usage_version" "$usage_run_mode" "$usage_clear_first" "${usage_webapi_storage_engine:-sqlite}" "${usage_telemetry:-local}" "${usage_webapi_search:-sql}" "${usage_crm:-noop}" "${usage_auth_provider:-local}" "${usage_webapi_email_sender:-noop}" "${usage_email_verification_strategy:-none}" +run_jupiter_webapp "$usage_universe" "$instance" "$webapi_port" "$webapi_postgres_port" "$api_port" "$webui_port" "$published_port" "$docs_port" "$mcp_port" no-wait monit dev "$usage_source" "$usage_version" "$usage_run_mode" "$usage_clear_first" "${usage_webapi_storage_engine:-sqlite}" "${usage_telemetry:-local}" "${usage_webapi_search:-sql}" "${usage_crm:-noop}" "${usage_auth_provider:-local}" "${usage_webapi_email_sender:-noop}" "${usage_email_verification_strategy:-none}" diff --git a/tasks/test/_common.sh b/tasks/test/_common.sh index 0083b0980..337d704a2 100644 --- a/tasks/test/_common.sh +++ b/tasks/test/_common.sh @@ -9,14 +9,15 @@ run_tests() { local api_url=$2 local mcp_url=$3 local webui_url=$4 - local docs_url=$5 - local email_verification_strategy=$6 - local auth_strategy=$7 - local headed=$8 - local filter_expr=$9 - shift 9 + local published_url=$5 + local docs_url=$6 + local email_verification_strategy=$7 + local auth_strategy=$8 + local headed=$9 + local filter_expr=${10} + shift 10 - log info "Running tests with Web API $webapi_url and API $api_url and MCP $mcp_url and Web UI $webui_url and Docs $docs_url and email verification strategy $email_verification_strategy and auth strategy $auth_strategy and pytest args ${*} and headed=${headed} filter=${filter_expr:-<none>}" + log info "Running tests with Web API $webapi_url and API $api_url and MCP $mcp_url and Web UI $webui_url and Published $published_url and Docs $docs_url and email verification strategy $email_verification_strategy and auth strategy $auth_strategy and pytest args ${*} and headed=${headed} filter=${filter_expr:-<none>}" if [[ -n "$RETRIES" ]]; then retries="$RETRIES" @@ -28,6 +29,7 @@ run_tests() { export API_URL=$api_url export MCP_URL=$mcp_url export WEBUI_URL=$webui_url + export PUBLISHED_URL=$published_url export DOCS_URL=$docs_url export ITEST_EMAIL_VERIFICATION_STRATEGY=$email_verification_strategy export ITEST_AUTH_STRATEGY=$auth_strategy diff --git a/tasks/test/int.sh b/tasks/test/int.sh index 72831dfbb..3a00bb03e 100755 --- a/tasks/test/int.sh +++ b/tasks/test/int.sh @@ -45,6 +45,7 @@ webapi_url=$(echo "$test_manifest" | jq -r '.webApiUrl') api_url=$(echo "$test_manifest" | jq -r '.apiUrl') mcp_url=$(echo "$test_manifest" | jq -r '.mcpUrl') docs_url=$(echo "$test_manifest" | jq -r '.docsUrl') +published_url=$(echo "$test_manifest" | jq -r '.publishedUrl') email_verification_strategy=$(echo "$test_manifest" | jq -r '.emailVerificationStrategy // "none"') auth_strategy=$(echo "$test_manifest" | jq -r '.authStrategy // "local"') @@ -57,4 +58,4 @@ wait_for_service_to_start "api" "$api_url" log info "Running tests with pytest args ${usage_pytest_args[*]}" -run_tests "$webapi_url" "$api_url" "$mcp_url" "$webui_url" "$docs_url" "$email_verification_strategy" "$auth_strategy" "$usage_headed" "$usage_filter" "${usage_pytest_args[@]}" +run_tests "$webapi_url" "$api_url" "$mcp_url" "$webui_url" "$published_url" "$docs_url" "$email_verification_strategy" "$auth_strategy" "$usage_headed" "$usage_filter" "${usage_pytest_args[@]}" diff --git a/tasks/test/matrix.sh b/tasks/test/matrix.sh index 68cf57bfd..98c6f6ce3 100755 --- a/tasks/test/matrix.sh +++ b/tasks/test/matrix.sh @@ -80,11 +80,12 @@ run_dev_matrix_cell() { local storage_engine=$2 local instance="${matrix_base}-${run_mode}-${storage_engine}" - local webapi_port webapi_postgres_port api_port webui_port docs_port mcp_port + local webapi_port webapi_postgres_port api_port webui_port published_port docs_port mcp_port webapi_port=$(get_free_port) webapi_postgres_port=$(get_free_port) api_port=$(get_free_port) webui_port=$(get_free_port) + published_port=$(get_free_port) docs_port=$(get_free_port) mcp_port=$(get_free_port) @@ -106,6 +107,7 @@ run_dev_matrix_cell() { "$webapi_postgres_port" \ "$api_port" \ "$webui_port" \ + "$published_port" \ "$docs_port" \ "$mcp_port" \ wait:all \ @@ -129,11 +131,12 @@ run_thrive_sh_test_matrix_cell() { local storage_engine=$1 local instance="${matrix_base}-thsh-${storage_engine}" - local webapi_port webapi_postgres_port api_port webui_port docs_port mcp_port + local webapi_port webapi_postgres_port api_port webui_port published_port docs_port mcp_port webapi_port=$(get_free_port) webapi_postgres_port=$(get_free_port) api_port=$(get_free_port) webui_port=$(get_free_port) + published_port=$(get_free_port) docs_port=$(get_free_port) mcp_port=$(get_free_port) @@ -155,6 +158,7 @@ run_thrive_sh_test_matrix_cell() { "$webapi_postgres_port" \ "$api_port" \ "$webui_port" \ + "$published_port" \ "$docs_port" \ "$mcp_port" \ wait:all \ From 412e532fae61ac7fe5a27c9c27b11ee471a824c1 Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Wed, 10 Jun 2026 17:31:25 +0300 Subject: [PATCH 18/21] Sepcial logic for handling cookie domain --- .../auth/sub/google/oauth-state.server.ts | 23 +++++++++----- src/core/jupiter/core/config-server.ts | 30 +++++++++++++++++++ src/webui/app/logic/config.server.ts | 3 ++ .../init/google/create-or-login-user.tsx | 7 ++++- .../app/lifecycle/init/google/prepare.tsx | 1 + .../app/routes/app/lifecycle/logout.server.ts | 5 +++- src/webui/app/sessions.ts | 4 +++ 7 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/core/jupiter/core/auth/sub/google/oauth-state.server.ts b/src/core/jupiter/core/auth/sub/google/oauth-state.server.ts index 0d6fd7f45..f8df9068e 100644 --- a/src/core/jupiter/core/auth/sub/google/oauth-state.server.ts +++ b/src/core/jupiter/core/auth/sub/google/oauth-state.server.ts @@ -2,26 +2,35 @@ import { createCookie } from "@remix-run/node"; import { GOOGLE_OAUTH_STATE_COOKIE_NAME } from "#/core/infra/names"; -// The cookie security setting is service-level configuration, so callers -// pass it in rather than core reaching into a service's config. -function googleOauthStateCookie(secure: boolean) { +// The cookie security setting and domain are service-level configuration, so +// callers pass them in rather than core reaching into a service's config. The +// domain pins the cookie to the WebUI host (e.g. app.get-thriving.com) so it is +// never shared with the apex domain or sibling services. +function googleOauthStateCookie(secure: boolean, domain?: string) { return createCookie(GOOGLE_OAUTH_STATE_COOKIE_NAME, { httpOnly: true, maxAge: 60 * 10, path: "/", sameSite: "lax", secure: secure, + domain: domain, }); } -export async function saveGoogleOauthState(state: string, secure: boolean) { - return await googleOauthStateCookie(secure).serialize(state); +export async function saveGoogleOauthState( + state: string, + secure: boolean, + domain?: string, +) { + return await googleOauthStateCookie(secure, domain).serialize(state); } export async function loadGoogleOauthState(cookieHeader: string | null) { return await googleOauthStateCookie(false).parse(cookieHeader); } -export async function clearGoogleOauthState(secure: boolean) { - return await googleOauthStateCookie(secure).serialize("", { maxAge: 0 }); +export async function clearGoogleOauthState(secure: boolean, domain?: string) { + return await googleOauthStateCookie(secure, domain).serialize("", { + maxAge: 0, + }); } diff --git a/src/core/jupiter/core/config-server.ts b/src/core/jupiter/core/config-server.ts index c185a566d..ca1c2d3db 100644 --- a/src/core/jupiter/core/config-server.ts +++ b/src/core/jupiter/core/config-server.ts @@ -86,6 +86,36 @@ export function resolveWebUiUrl( return webUiUrl; } +// The domain to scope the WebUI's session and auth cookies to. We deliberately +// pin this to the exact WebUI host (e.g. `app.get-thriving.com`) so the cookies +// are NEVER shared with the apex domain (`get-thriving.com`) or sibling services +// like the published site. Returning `undefined` yields a host-only cookie, +// which is what we want for local development and raw IP hosts (browsers reject +// or needlessly broaden an explicit Domain there). +export function resolveSessionCookieDomain( + globalProperties: GlobalPropertiesServer, +): string | undefined { + let host: string; + try { + host = new URL(resolveWebUiUrl(globalProperties)).hostname; + } catch { + return undefined; + } + + const isIpv4 = /^\d{1,3}(\.\d{1,3}){3}$/.test(host); + if ( + host === "localhost" || + host === "0.0.0.0" || + host === "::1" || + host.endsWith(".localhost") || + isIpv4 + ) { + return undefined; + } + + return host; +} + export function resolvePublishedUrl( globalProperties: GlobalPropertiesServer, ): string { diff --git a/src/webui/app/logic/config.server.ts b/src/webui/app/logic/config.server.ts index 6902ce25c..1e4f1cba4 100644 --- a/src/webui/app/logic/config.server.ts +++ b/src/webui/app/logic/config.server.ts @@ -4,6 +4,7 @@ import { GLOBAL_PROPERTIES, logServiceStartupBanner, resolvePublishedUrl, + resolveSessionCookieDomain, resolveWebUiUrl, } from "@jupiter/core/config-server"; import type { FrontDoorInfo } from "@jupiter/core/frontdoor"; @@ -22,6 +23,7 @@ export interface ServicePropertiesServer { pwaStartUrl: string; sessionCookieSecure: boolean; sessionCookieSecret: string; + sessionCookieDomain: string | undefined; inboxTasksToAskForGC: number; overdueInfoDays: number; overdueWarningDays: number; @@ -54,6 +56,7 @@ function loadServicePropertiesOnServer(): ServicePropertiesServer { pwaStartUrl: process.env.PWA_START_URL as string, sessionCookieSecure: process.env.SESSION_COOKIE_SECURE === "true", sessionCookieSecret: process.env.SESSION_COOKIE_SECRET as string, + sessionCookieDomain: resolveSessionCookieDomain(GLOBAL_PROPERTIES), inboxTasksToAskForGC: parseInt( process.env.INBOX_TASKS_TO_ASK_FOR_GC as string, 10, diff --git a/src/webui/app/routes/app/lifecycle/init/google/create-or-login-user.tsx b/src/webui/app/routes/app/lifecycle/init/google/create-or-login-user.tsx index e145de1e5..6d21fbf34 100644 --- a/src/webui/app/routes/app/lifecycle/init/google/create-or-login-user.tsx +++ b/src/webui/app/routes/app/lifecycle/init/google/create-or-login-user.tsx @@ -49,6 +49,7 @@ export async function loader({ request }: LoaderFunctionArgs) { headers: { "Set-Cookie": await clearGoogleOauthState( SERVICE_PROPERTIES.sessionCookieSecure, + SERVICE_PROPERTIES.sessionCookieDomain, ), }, }); @@ -76,7 +77,10 @@ export async function loader({ request }: LoaderFunctionArgs) { headers.append("Set-Cookie", await commitSession(session)); headers.append( "Set-Cookie", - await clearGoogleOauthState(SERVICE_PROPERTIES.sessionCookieSecure), + await clearGoogleOauthState( + SERVICE_PROPERTIES.sessionCookieSecure, + SERVICE_PROPERTIES.sessionCookieDomain, + ), ); return redirect(emailVerificationVerifyUrl(result.new_user.ref_id), { @@ -88,6 +92,7 @@ export async function loader({ request }: LoaderFunctionArgs) { headers: { "Set-Cookie": await clearGoogleOauthState( SERVICE_PROPERTIES.sessionCookieSecure, + SERVICE_PROPERTIES.sessionCookieDomain, ), }, }); diff --git a/src/webui/app/routes/app/lifecycle/init/google/prepare.tsx b/src/webui/app/routes/app/lifecycle/init/google/prepare.tsx index f461b0e80..9e356b265 100644 --- a/src/webui/app/routes/app/lifecycle/init/google/prepare.tsx +++ b/src/webui/app/routes/app/lifecycle/init/google/prepare.tsx @@ -64,6 +64,7 @@ export async function loader({ request }: LoaderFunctionArgs) { "Set-Cookie": await saveGoogleOauthState( result.state, SERVICE_PROPERTIES.sessionCookieSecure, + SERVICE_PROPERTIES.sessionCookieDomain, ), }, }); diff --git a/src/webui/app/routes/app/lifecycle/logout.server.ts b/src/webui/app/routes/app/lifecycle/logout.server.ts index 90654ab9e..7df21c92c 100644 --- a/src/webui/app/routes/app/lifecycle/logout.server.ts +++ b/src/webui/app/routes/app/lifecycle/logout.server.ts @@ -15,7 +15,10 @@ export async function logoutAndRedirectToLogin(request: Request) { headers.append("Set-Cookie", await destroySession(session)); headers.append( "Set-Cookie", - await clearGoogleOauthState(SERVICE_PROPERTIES.sessionCookieSecure), + await clearGoogleOauthState( + SERVICE_PROPERTIES.sessionCookieSecure, + SERVICE_PROPERTIES.sessionCookieDomain, + ), ); return redirectDocument(LOGIN_URL, { headers }); diff --git a/src/webui/app/sessions.ts b/src/webui/app/sessions.ts index 47f8d7059..effff620c 100644 --- a/src/webui/app/sessions.ts +++ b/src/webui/app/sessions.ts @@ -27,6 +27,10 @@ const { getSession, commitSession, destroySession } = path: "/", sameSite: "lax", // Not strict because of https://github.com/oauth2-proxy/oauth2-proxy/issues/830 secure: SERVICE_PROPERTIES.sessionCookieSecure, + // Pin to the WebUI host (e.g. app.get-thriving.com) so the session is + // never shared with the apex domain or other subdomains. Undefined in + // local dev yields a host-only cookie. + domain: SERVICE_PROPERTIES.sessionCookieDomain, secrets: [SERVICE_PROPERTIES.sessionCookieSecret], }, }); From a64f5911eee8659f7189f014b7d090fa6eb89032 Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Wed, 10 Jun 2026 23:44:57 +0300 Subject: [PATCH 19/21] Small phix --- render.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/render.yaml b/render.yaml index c2ef77781..d0fc145f9 100644 --- a/render.yaml +++ b/render.yaml @@ -130,7 +130,10 @@ services: value: verify previewValue: none - key: PUBLISHED_URL - value: https://published.get-thriving.com + fromService: + name: jupiter-published + type: web + envVarKey: RENDER_EXTERNAL_URL - type: web name: jupiter-published buildFilter: From 3fb610f22c9a7edbc48b0582f39d9a08d91d0f7b Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Thu, 11 Jun 2026 23:28:08 +0300 Subject: [PATCH 20/21] Fixes --- itests/helpers.py | 5 ++ itests/webui/entities/docs.test.py | 15 +++- itests/webui/entities/metrics.test.py | 7 ++ itests/webui/entities/schedules.test.py | 7 ++ itests/webui/entities/smart_list.test.py | 9 ++- .../infra/component/layout/trunk-panel.tsx | 74 ++++++++++++++++--- .../core/time_plans/component/editor.tsx | 30 ++++---- .../core/vacations/component/editor.tsx | 28 ++++--- .../app/routes/publish/$externalId.tsx | 6 +- .../app/routes/app/workspace/docs/$dirId.tsx | 18 ++--- .../app/routes/app/workspace/working-mem.tsx | 18 ++--- 11 files changed, 146 insertions(+), 71 deletions(-) diff --git a/itests/helpers.py b/itests/helpers.py index 9abd95c35..8a871e9f1 100644 --- a/itests/helpers.py +++ b/itests/helpers.py @@ -61,6 +61,11 @@ def open_branch_publish_panel(page: Page, publish_section_id: str) -> None: open_entity_publish_panel(page, publish_section_id, "branch-entity-publish") +def open_trunk_publish_panel(page: Page, publish_section_id: str) -> None: + """Open the publish panel in a trunk entity view.""" + open_entity_publish_panel(page, publish_section_id, "trunk-entity-publish") + + def open_entity_publish_panel( page: Page, publish_section_id: str, publish_button_id: str ) -> None: diff --git a/itests/webui/entities/docs.test.py b/itests/webui/entities/docs.test.py index 64675165b..1042fffd1 100644 --- a/itests/webui/entities/docs.test.py +++ b/itests/webui/entities/docs.test.py @@ -46,6 +46,7 @@ from itests.helpers import ( get_parsed_from_response, open_leaf_publish_panel, + open_trunk_publish_panel, type_editorjs_content_and_wait_for_save, ) @@ -385,16 +386,24 @@ def test_webui_dir_publish_and_view_public(page: Page, create_dir, create_doc) - page.goto(f"/app/workspace/docs/{folder.ref_id}") page.wait_for_selector("#trunk-panel") - page.wait_for_selector("#Dir-publish") + open_trunk_publish_panel(page, "Dir-publish") page.locator("button[id='Dir-publish-create']").click() page.wait_for_url(re.compile(rf"/app/workspace/docs/{folder.ref_id}")) - page.wait_for_selector("#Dir-publish") + page.wait_for_selector("#trunk-panel") + + open_trunk_publish_panel(page, "Dir-publish") expect(page.locator("#Dir-publish")).to_contain_text("draft") page.locator("button[id='Dir-publish-toggle-status']").click() page.wait_for_url(re.compile(rf"/app/workspace/docs/{folder.ref_id}")) - page.wait_for_selector("#Dir-publish") + page.wait_for_selector("#trunk-panel") + + # Wait until the activation has actually committed (the panel reflects the + # active status) before navigating to the public URL. Otherwise the guest + # load can race the activation and 404, since publishEntityLoadByExternalId + # only serves active entities. + open_trunk_publish_panel(page, "Dir-publish") expect(page.locator("#Dir-publish")).to_contain_text("active") public_url = page.locator('input[name="publicUrl"]').input_value() diff --git a/itests/webui/entities/metrics.test.py b/itests/webui/entities/metrics.test.py index 5263af7f5..cf54743e3 100644 --- a/itests/webui/entities/metrics.test.py +++ b/itests/webui/entities/metrics.test.py @@ -242,6 +242,13 @@ def test_webui_metric_entry_view_public( page.wait_for_url(re.compile(rf"/app/workspace/metrics/{metric.ref_id}")) page.wait_for_selector("#branch-panel") + # Wait until the activation has actually committed (the panel reflects the + # active status) before navigating to the public URL. Otherwise the guest + # load can race the activation and 404, since publishEntityLoadByExternalId + # only serves active entities. + open_branch_publish_panel(page, "Metric-publish") + expect(page.locator("#Metric-publish")).to_contain_text("active") + public_url = page.locator('input[name="publicUrl"]').input_value() page.goto(public_url) page.wait_for_url(re.compile(r"/publish/metric/")) diff --git a/itests/webui/entities/schedules.test.py b/itests/webui/entities/schedules.test.py index 92c6a04b4..cc1b71ba1 100644 --- a/itests/webui/entities/schedules.test.py +++ b/itests/webui/entities/schedules.test.py @@ -266,6 +266,13 @@ def test_webui_schedule_stream_publish_and_view_public( ) page.wait_for_selector("#leaf-panel") + # Wait until the activation has actually committed (the panel reflects the + # active status) before navigating to the public URL. Otherwise the guest + # load can race the activation and 404, since publishEntityLoadByExternalId + # only serves active entities. + open_leaf_publish_panel(page, "ScheduleStream-publish") + expect(page.locator("#ScheduleStream-publish")).to_contain_text("active") + public_url = page.locator('input[name="publicUrl"]').input_value() assert "/publish/" in public_url diff --git a/itests/webui/entities/smart_list.test.py b/itests/webui/entities/smart_list.test.py index 0f1ce8b1d..dbb278e47 100644 --- a/itests/webui/entities/smart_list.test.py +++ b/itests/webui/entities/smart_list.test.py @@ -167,7 +167,7 @@ def test_webui_smart_list_publish_and_view_public( page.goto(public_url) page.wait_for_url(re.compile(r"/publish/smart-list/")) - page.wait_for_selector("#branch-panel") + page.wait_for_selector("#leaf-panel") expect(page.locator(f"#smart-list-item-{item.ref_id}")).to_contain_text( "Published Smart List Item" @@ -192,6 +192,13 @@ def test_webui_smart_list_item_view_public( page.wait_for_url(re.compile(rf"/app/workspace/smart-lists/{smart_list.ref_id}")) page.wait_for_selector("#branch-panel") + # Wait until the activation has actually committed (the panel reflects the + # active status) before navigating to the public URL. Otherwise the guest + # load can race the activation and 404, since publishEntityLoadByExternalId + # only serves active entities. + open_branch_publish_panel(page, "SmartList-publish") + expect(page.locator("#SmartList-publish")).to_contain_text("active") + public_url = page.locator('input[name="publicUrl"]').input_value() page.goto(public_url) page.wait_for_url(re.compile(r"/publish/smart-list/")) diff --git a/src/core/jupiter/core/infra/component/layout/trunk-panel.tsx b/src/core/jupiter/core/infra/component/layout/trunk-panel.tsx index 4ae7ef43f..b2a46084e 100644 --- a/src/core/jupiter/core/infra/component/layout/trunk-panel.tsx +++ b/src/core/jupiter/core/infra/component/layout/trunk-panel.tsx @@ -3,6 +3,7 @@ import { ArrowDownward as ArrowDownwardIcon, ArrowUpward as ArrowUpwardIcon, Close as CloseIcon, + Public as PublicIcon, } from "@mui/icons-material"; import { Box, @@ -15,13 +16,20 @@ import { import { Link, useLocation } from "@remix-run/react"; import { motion, useIsPresent } from "framer-motion"; import type { PropsWithChildren } from "react"; -import { useCallback, useEffect, useRef } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; +import type { + EntityId, + NamedEntityTag, + PublishEntity, +} from "@jupiter/webapi-client"; +import { PublishPanel } from "#/core/common/sub/publish/components/publish-panel"; import { extractTrunkFromPath } from "#/core/infra/routes"; import { restoreScrollPosition, saveScrollPosition, } from "#/core/infra/scroll-restoration"; +import { TopLevelInfoContext } from "#/core/infra/top-level-context"; import { useBigScreen } from "#/core/infra/component/use-big-screen"; import { useHydrated } from "#/core/infra/component/use-hidrated"; import { @@ -36,6 +44,11 @@ interface TrunkPanelProps { createLocation?: string; actions?: JSX.Element; returnLocation: string; + entityType?: NamedEntityTag; + entityRefId?: EntityId; + inputsEnabled?: boolean; + publishable?: boolean; + publishEntity?: PublishEntity; } export function TrunkPanel(props: PropsWithChildren<TrunkPanelProps>) { @@ -47,6 +60,13 @@ export function TrunkPanel(props: PropsWithChildren<TrunkPanelProps>) { const shouldShowALeaflet = useLeafNeedsToShowLeaflet(); const shouldShowALeaf = useTrunkNeedsToShowLeaf(); const shouldShowABranch = useTrunkNeedsToShowBranch(); + const topLevelInfo = useContext(TopLevelInfoContext); + const [showPublish, setShowPublish] = useState(false); + + const hasPublish = + props.publishable === true && + props.entityType !== undefined && + props.entityRefId !== undefined; // This little function is a hack to get around the fact that Framer Motion // generates a translateX(Xpx) CSS applied to the StyledMotionDrawer element. @@ -183,7 +203,18 @@ export function TrunkPanel(props: PropsWithChildren<TrunkPanelProps>) { {props.actions} - <IconButton sx={{ marginLeft: "auto" }}> + {hasPublish && ( + <Box sx={{ marginLeft: "auto", display: "flex" }}> + <IconButton + id="trunk-entity-publish" + onClick={() => setShowPublish((p) => !p)} + > + <PublicIcon color={showPublish ? "primary" : undefined} /> + </IconButton> + </Box> + )} + + <IconButton sx={{ marginLeft: hasPublish ? undefined : "auto" }}> <Link to={props.returnLocation}> <CloseIcon /> </Link> @@ -192,15 +223,36 @@ export function TrunkPanel(props: PropsWithChildren<TrunkPanelProps>) { </TrunkPanelControls> )} - <TrunkPanelContent - id="trunk-panel-content" - ref={containerRef} - isbigscreen={isBigScreen ? "true" : "false"} - hasbranch={shouldShowABranch ? "true" : "false"} - hasleaf={shouldShowALeaf || shouldShowALeaflet ? "true" : "false"} - > - <Stack spacing={2}>{props.children}</Stack> - </TrunkPanelContent> + {showPublish && hasPublish ? ( + <TrunkPanelContent + id="trunk-panel-content" + ref={containerRef} + isbigscreen={isBigScreen ? "true" : "false"} + hasbranch={shouldShowABranch ? "true" : "false"} + hasleaf={shouldShowALeaf || shouldShowALeaflet ? "true" : "false"} + > + <Stack spacing={2}> + <PublishPanel + entityType={props.entityType!} + entityRefId={props.entityRefId!} + topLevelInfo={topLevelInfo} + inputsEnabled={props.inputsEnabled ?? false} + publishEntity={props.publishEntity ?? null} + /> + </Stack> + <Box sx={{ height: "4rem" }}></Box> + </TrunkPanelContent> + ) : ( + <TrunkPanelContent + id="trunk-panel-content" + ref={containerRef} + isbigscreen={isBigScreen ? "true" : "false"} + hasbranch={shouldShowABranch ? "true" : "false"} + hasleaf={shouldShowALeaf || shouldShowALeaflet ? "true" : "false"} + > + <Stack spacing={2}>{props.children}</Stack> + </TrunkPanelContent> + )} </TrunkPanelFrame> ); } diff --git a/src/core/jupiter/core/time_plans/component/editor.tsx b/src/core/jupiter/core/time_plans/component/editor.tsx index c83217a93..a488197d9 100644 --- a/src/core/jupiter/core/time_plans/component/editor.tsx +++ b/src/core/jupiter/core/time_plans/component/editor.tsx @@ -62,22 +62,20 @@ export function TimePlanEditor(props: TimePlanEditorProps) { <SectionCard title="Properties" actions={ - props.inputsEnabled ? ( - <SectionActions - id="time-plan-properties" - topLevelInfo={props.topLevelInfo} - inputsEnabled={props.inputsEnabled} - actions={[ - ActionSingle({ - id: "time-plan-change-time-config", - text: "Change Time Config", - value: changeTimeConfigIntent, - disabled: !props.inputsEnabled, - highlight: true, - }), - ]} - /> - ) : undefined + <SectionActions + id="time-plan-properties" + topLevelInfo={props.topLevelInfo} + inputsEnabled={props.inputsEnabled} + actions={[ + ActionSingle({ + id: "time-plan-change-time-config", + text: "Change Time Config", + value: changeTimeConfigIntent, + disabled: !props.inputsEnabled, + highlight: true, + }), + ]} + /> } > <Stack direction={isBigScreen ? "row" : "column"} spacing={2} useFlexGap> diff --git a/src/core/jupiter/core/vacations/component/editor.tsx b/src/core/jupiter/core/vacations/component/editor.tsx index bf72fdec6..49aea60f8 100644 --- a/src/core/jupiter/core/vacations/component/editor.tsx +++ b/src/core/jupiter/core/vacations/component/editor.tsx @@ -35,21 +35,19 @@ export function VacationEditor(props: VacationEditorProps) { <SectionCard title="Properties" actions={ - props.inputsEnabled ? ( - <SectionActions - id="vacation-update" - topLevelInfo={props.topLevelInfo} - inputsEnabled={props.inputsEnabled} - actions={[ - ActionSingle({ - id: "vacation-update", - text: "Save", - value: "update", - highlight: true, - }), - ]} - /> - ) : undefined + <SectionActions + id="vacation-update" + topLevelInfo={props.topLevelInfo} + inputsEnabled={props.inputsEnabled} + actions={[ + ActionSingle({ + id: "vacation-update", + text: "Save", + value: "update", + highlight: true, + }), + ]} + /> } > <Stack direction="row" spacing={1}> diff --git a/src/published/app/routes/publish/$externalId.tsx b/src/published/app/routes/publish/$externalId.tsx index eba691ff2..78af45fa6 100644 --- a/src/published/app/routes/publish/$externalId.tsx +++ b/src/published/app/routes/publish/$externalId.tsx @@ -69,8 +69,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) { external_id: externalId, }); + // Preserve any query string (e.g. calendar date/period/view) so that + // shareable deep links survive the redirect to the entity-specific route. + const { search } = new URL(request.url); + return redirect( - publishedEntityLocation(externalId, result.publish_entity.owner), + publishedEntityLocation(externalId, result.publish_entity.owner) + search, ); } catch (error) { handlePublishedLoaderError(error); diff --git a/src/webui/app/routes/app/workspace/docs/$dirId.tsx b/src/webui/app/routes/app/workspace/docs/$dirId.tsx index 864bf930b..e9ab16253 100644 --- a/src/webui/app/routes/app/workspace/docs/$dirId.tsx +++ b/src/webui/app/routes/app/workspace/docs/$dirId.tsx @@ -9,7 +9,7 @@ import { import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; -import { Form, Outlet } from "@remix-run/react"; +import { Outlet } from "@remix-run/react"; import { CreateNewFolder as CreateNewFolderIcon, Settings as SettingsIcon, @@ -22,7 +22,6 @@ import { useContext, useMemo, useState } from "react"; import { z } from "zod"; import { parseForm, parseParams } from "zodix"; import { isDirRoot } from "@jupiter/core/docs/sub/dir/root"; -import { PublishPanel } from "@jupiter/core/common/sub/publish/components/publish-panel"; import { EntityNameOneLineComponent } from "@jupiter/core/common/component/entity-name"; import { EntityNoNothingCard } from "@jupiter/core/infra/component/entity-no-nothing-card"; import { @@ -207,6 +206,11 @@ export default function DocsInFolder() { key={`docs-dir-${dirId}`} createLocation={`/app/workspace/docs/${dirId}/doc/new`} returnLocation="/app/workspace" + entityType={NamedEntityTag.DIR} + entityRefId={dirId} + inputsEnabled={true} + publishable + publishEntity={loaderData.publishEntity ?? undefined} actions={ <SectionActions id="docs-actions" @@ -273,16 +277,6 @@ export default function DocsInFolder() { } > <NestingAwareBlock shouldHide={shouldShowALeaf}> - <Form method="post"> - <PublishPanel - entityType={NamedEntityTag.DIR} - entityRefId={dirId} - topLevelInfo={topLevelInfo} - inputsEnabled={true} - publishEntity={loaderData.publishEntity} - /> - </Form> - {listIsEmpty && !showParentLink && ( <EntityNoNothingCard title="You Have To Start Somewhere" diff --git a/src/webui/app/routes/app/workspace/working-mem.tsx b/src/webui/app/routes/app/workspace/working-mem.tsx index a08722154..31ce72d1a 100644 --- a/src/webui/app/routes/app/workspace/working-mem.tsx +++ b/src/webui/app/routes/app/workspace/working-mem.tsx @@ -3,13 +3,12 @@ import TuneIcon from "@mui/icons-material/Tune"; import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import type { ShouldRevalidateFunction } from "@remix-run/react"; -import { Form, Outlet, useNavigation } from "@remix-run/react"; +import { Outlet, useNavigation } from "@remix-run/react"; import { AnimatePresence } from "framer-motion"; import { StatusCodes } from "http-status-codes"; import { useContext } from "react"; import { z } from "zod"; import { parseForm } from "zodix"; -import { PublishPanel } from "@jupiter/core/common/sub/publish/components/publish-panel"; import { EntityNoteEditor } from "@jupiter/core/infra/component/entity-note-editor"; import { makeTrunkErrorBoundary } from "@jupiter/core/infra/component/error-boundary"; import { NestingAwareBlock } from "@jupiter/core/infra/component/layout/nesting-aware-block"; @@ -119,6 +118,11 @@ export default function WorkingMem() { <TrunkPanel key={"working-mem"} returnLocation="/app/workspace" + entityType={NamedEntityTag.WORKING_MEM} + entityRefId={loaderData.entry.working_mem.ref_id} + inputsEnabled={inputsEnabled} + publishable + publishEntity={loaderData.entry.publish_entity ?? undefined} actions={ <SectionActions id="working-mem-actions" @@ -139,16 +143,6 @@ export default function WorkingMem() { shouldHide={shouldShowABranch || shouldShowALeafToo} > <ToolPanel> - <Form method="post"> - <PublishPanel - entityType={NamedEntityTag.WORKING_MEM} - entityRefId={loaderData.entry.working_mem.ref_id} - topLevelInfo={topLevelInfo} - inputsEnabled={inputsEnabled} - publishEntity={loaderData.entry.publish_entity ?? null} - /> - </Form> - <SectionCard title="Working Mem"> <EntityNoteEditor initialNote={loaderData.entry.note} From 5af1cb8577c253ffd86d75658ba4cc5986a9d8ca Mon Sep 17 00:00:00 2001 From: Mike Bestcat <mike@get-thriving.com> Date: Fri, 12 Jun 2026 12:43:15 +0300 Subject: [PATCH 21/21] Fixes for self-hosted --- infra/self-hosted/compose.yaml | 4 ++-- itests/webui/entities/life_plan.test.py | 8 ++++++++ itests/webui/entities/time_plans.test.py | 15 ++++++++++++--- .../core/working_mem/use_case/load_current.py | 4 +--- src/published/remix.config.js | 7 +++++++ tasks/_common.sh | 4 ++-- 6 files changed, 32 insertions(+), 10 deletions(-) diff --git a/infra/self-hosted/compose.yaml b/infra/self-hosted/compose.yaml index 4848aa50d..a9f715c06 100644 --- a/infra/self-hosted/compose.yaml +++ b/infra/self-hosted/compose.yaml @@ -192,7 +192,7 @@ services: - PORT=2000 - WEBAPI_SERVER_HOST=webapi - WEBAPI_SERVER_PORT=2000 - - PUBLISHED_URL=${PUBLISHED_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}/publish} + - PUBLISHED_URL=${PUBLISHED_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}} - DOCS_URL=${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}/docs/ restart: always image: ${DOCKER_IMAGE_PUBLISHED:-getthriving/published:${VERSION:-latest}} @@ -222,7 +222,7 @@ services: - API_URL=${API_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}}/api - MCP_URL=${MCP_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}}/mcp - WEBUI_URL=${WEBUI_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}} - - PUBLISHED_URL=${PUBLISHED_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}/publish} + - PUBLISHED_URL=${PUBLISHED_SERVER_URL:-${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}} - DOCS_URL=${PROTOCOL:-https}://${DOMAIN:?Check https://docs.get-thriving.com/how-tos/self-hosting}/docs/ - SESSION_COOKIE_SECURE=${SESSION_COOKIE_SECURE:-false} - SESSION_COOKIE_SECRET=${SESSION_COOKIE_SECRET:?Check https://docs.get-thriving.com/how-tos/self-hosting} diff --git a/itests/webui/entities/life_plan.test.py b/itests/webui/entities/life_plan.test.py index e5e510433..a71d8d62a 100644 --- a/itests/webui/entities/life_plan.test.py +++ b/itests/webui/entities/life_plan.test.py @@ -365,6 +365,14 @@ def test_webui_life_plan_aspects_update(page: Page, create_aspect) -> None: page.locator("#aspect-properties button", has_text="Save").click() page.wait_for_url(f"/app/workspace/life-plan/aspects/{aspect.ref_id}") + # Reload to read the persisted value from the server instead of the in-place + # form state. The aspect update redirects back to the same URL, so the input + # is not remounted from a fresh mount; a post-save revalidation animation can + # briefly render stale loader data and freeze the field's defaultValue at the + # old value, making the in-place assertion flaky. Reloading forces a clean + # load of the persisted aspect, matching the todo update test pattern. + page.reload() + page.wait_for_selector("#leaf-panel") expect(page.locator('input[name="name"]')).to_have_value("Aspect Lifecycle Updated") diff --git a/itests/webui/entities/time_plans.test.py b/itests/webui/entities/time_plans.test.py index c61caa528..576d66bd6 100644 --- a/itests/webui/entities/time_plans.test.py +++ b/itests/webui/entities/time_plans.test.py @@ -2467,9 +2467,18 @@ def test_webui_time_plan_generate_no_nothing_and_regenerate(page: Page) -> None: def test_webui_time_plan_generate_does_not_override_existing_time_plans( page: Page, create_time_plan ) -> None: - # We add a couple of days in advance here to match the behaviour of the gen tool. - right_now = pendulum.now(tz="UTC").add(days=3) - _ = create_time_plan(right_now.strftime("%Y-%m-%d"), RecurringTaskPeriod.WEEKLY) + # Gen targets the week of (today + generation_in_advance_days[WEEKLY]). That + # target always falls in either the current ISO week or the next one, but a + # single "now + 3" can straddle a week boundary (e.g. when run late in the + # week) and land in a different week than gen's target, making the test + # flaky. Pre-create a user weekly plan for both the current week and the + # next week so gen's target week always already has a user plan and is + # therefore skipped (no "Make weekly plan" task generated). + now = pendulum.now(tz="UTC") + _ = create_time_plan(now.strftime("%Y-%m-%d"), RecurringTaskPeriod.WEEKLY) + _ = create_time_plan( + now.add(days=7).strftime("%Y-%m-%d"), RecurringTaskPeriod.WEEKLY + ) page.goto("/app/workspace/time-plans/settings") diff --git a/src/core/jupiter/core/working_mem/use_case/load_current.py b/src/core/jupiter/core/working_mem/use_case/load_current.py index 422f53573..bfde325bf 100644 --- a/src/core/jupiter/core/working_mem/use_case/load_current.py +++ b/src/core/jupiter/core/working_mem/use_case/load_current.py @@ -80,9 +80,7 @@ async def _perform_transactional_read( EntityLink.std(NamedEntityTag.WORKING_MEM.value, working_mem.ref_id), allow_archived=True, ) - publish_entity = await uow.get( - PublishEntityRepository - ).load_optional_for_owner( + publish_entity = await uow.get(PublishEntityRepository).load_optional_for_owner( EntityLink.std(NamedEntityTag.WORKING_MEM.value, working_mem.ref_id), allow_archived=False, ) diff --git a/src/published/remix.config.js b/src/published/remix.config.js index 0e83f8dd9..fce48a8c4 100644 --- a/src/published/remix.config.js +++ b/src/published/remix.config.js @@ -4,6 +4,13 @@ const { createRoutesFromFolders } = require("@remix-run/v1-route-convention"); const config = { ignoredRouteFiles: ["**/.*"], serverModuleFormat: "cjs", + // The published service always lives under the `/publish` path: its routes are + // `/publish/*` and, in self-hosted mode, nginx mounts it at `/publish` on the + // shared domain (without stripping the prefix). Serve the client bundle under + // `/publish/build/` too, so asset requests stay within that mount instead of + // hitting the root (which nginx routes to the WebUI). Assets are still written + // to `public/build`; only the URL prefix changes. + publicPath: "/publish/build/", routes(defineRoutes) { return createRoutesFromFolders(defineRoutes); }, diff --git a/tasks/_common.sh b/tasks/_common.sh index a9d377d00..6ead3a73f 100644 --- a/tasks/_common.sh +++ b/tasks/_common.sh @@ -1164,13 +1164,13 @@ $(_thrive_sh_test_default_docker_image_env_append_ssh "$version" arm64 | sed 's/ echo \"WEBAPI_EMAIL_SENDER=${webapi_email_sender}\" >> .env && echo \"POSTGRES_VERSION=${POSTGRES_VERSION}\" >> .env && echo \"WEBAPI_CRON_EXECUTION_MODE=${WEBAPI_CRON_EXECUTION_MODE_LOCAL}\" >> .env && - echo \"DOCKER_IMAGE_WEBAPI=jupiter/webapi:${version}-arm64\" >> .env && + echo \"DOCKER_IMAGE_WEBAPI=jupiter/webapi-srv:${version}-arm64\" >> .env && echo \"DOCKER_IMAGE_API=jupiter/api:${version}-arm64\" >> .env && echo \"DOCKER_IMAGE_WEBUI=jupiter/webui:${version}-arm64\" >> .env && echo \"DOCKER_IMAGE_PUBLISHED=jupiter/published:${version}-arm64\" >> .env && echo \"DOCKER_IMAGE_DOCS=jupiter/docs:${version}-arm64\" >> .env && echo \"PUBLISHED_PORT=${PUBLISHED_TESTING_PORT}\" >> .env && - echo \"PUBLISHED_SERVER_URL=https://${gcp_dns_name}/publish\" >> .env && + echo \"PUBLISHED_SERVER_URL=https://${gcp_dns_name}\" >> .env && echo \"DOCKER_IMAGE_MCP=jupiter/mcp:${version}-arm64\" >> .env && $(for folder in "${WEBAPI_CRON_FOLDERS[@]}"; do cron_env_var=$(jupiter_webapi_cron_docker_env_var "$folder")