Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions canvas_generated/messages/effects_pb2.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions canvas_generated/messages/effects_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ class EffectType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
COMMAND_AVAILABLE_ACTIONS_RESULTS: _ClassVar[EffectType]
COMMAND_VALIDATION_ERRORS: _ClassVar[EffectType]
SHOW_ACTION_BUTTON: _ClassVar[EffectType]
SHOW_APPLICATION: _ClassVar[EffectType]
PATIENT_PORTAL__FORM_RESULT: _ClassVar[EffectType]
PATIENT_PORTAL__APPOINTMENT_IS_CANCELABLE: _ClassVar[EffectType]
PATIENT_PORTAL__APPOINTMENT_IS_RESCHEDULABLE: _ClassVar[EffectType]
Expand Down Expand Up @@ -559,6 +560,7 @@ ENTER_IN_ERROR_UNCATEGORIZED_DOCUMENT_REVIEW_COMMAND: EffectType
COMMAND_AVAILABLE_ACTIONS_RESULTS: EffectType
COMMAND_VALIDATION_ERRORS: EffectType
SHOW_ACTION_BUTTON: EffectType
SHOW_APPLICATION: EffectType
PATIENT_PORTAL__FORM_RESULT: EffectType
PATIENT_PORTAL__APPOINTMENT_IS_CANCELABLE: EffectType
PATIENT_PORTAL__APPOINTMENT_IS_RESCHEDULABLE: EffectType
Expand Down
4 changes: 2 additions & 2 deletions canvas_generated/messages/events_pb2.py

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions canvas_generated/messages/events_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,7 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
PLUGIN_UPDATED: _ClassVar[EventType]
APPLICATION__ON_OPEN: _ClassVar[EventType]
APPLICATION__ON_CONTEXT_CHANGE: _ClassVar[EventType]
APPLICATION__ON_GET: _ClassVar[EventType]
PATIENT_PORTAL__GET_FORMS: _ClassVar[EventType]
PATIENT_PORTAL__APPOINTMENT_CANCELED: _ClassVar[EventType]
PATIENT_PORTAL__APPOINTMENT_RESCHEDULED: _ClassVar[EventType]
Expand Down Expand Up @@ -2095,6 +2096,7 @@ PLUGIN_CREATED: EventType
PLUGIN_UPDATED: EventType
APPLICATION__ON_OPEN: EventType
APPLICATION__ON_CONTEXT_CHANGE: EventType
APPLICATION__ON_GET: EventType
PATIENT_PORTAL__GET_FORMS: EventType
PATIENT_PORTAL__APPOINTMENT_CANCELED: EventType
PATIENT_PORTAL__APPOINTMENT_RESCHEDULED: EventType
Expand Down
36 changes: 36 additions & 0 deletions canvas_sdk/effects/show_application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Any

from pydantic import Field

from canvas_generated.messages.effects_pb2 import EffectType
from canvas_sdk.effects.base import _BaseEffect


class ShowApplicationEffect(_BaseEffect):
"""An Effect that returns an application's properties."""

class Meta:
effect_type = EffectType.SHOW_APPLICATION

name: str = Field(min_length=1)
identifier: str = Field(min_length=1)
open_by_default: bool = Field(default=False)
priority: int = Field(default=0)

@property
def values(self) -> dict[str, Any]:
"""The ShowApplicationEffect's values."""
return {
"name": self.name,
"identifier": self.identifier,
"open_by_default": self.open_by_default,
"priority": self.priority,
}

@property
def effect_payload(self) -> dict[str, Any]:
"""The payload of the effect."""
return {"data": self.values}


__exports__ = ("ShowApplicationEffect",)
91 changes: 72 additions & 19 deletions canvas_sdk/handlers/application.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import importlib.metadata
from abc import ABC, abstractmethod
from enum import StrEnum

import deprecation

from canvas_sdk.effects import Effect
from canvas_sdk.effects.show_application import ShowApplicationEffect
from canvas_sdk.events import EventType
from canvas_sdk.handlers import BaseHandler
from canvas_sdk.handlers.action_button import ActionButton
from canvas_sdk.handlers.utils import normalize_effects

version = importlib.metadata.version("canvas")


class Application(BaseHandler, ABC):
"""An embeddable application that can be registered to Canvas."""

RESPONDS_TO = [
EventType.Name(EventType.APPLICATION__ON_OPEN),
EventType.Name(EventType.APPLICATION__ON_CONTEXT_CHANGE),
EventType.Name(EventType.APPLICATION__ON_GET),
]

def compute(self) -> list[Effect]:
Expand Down Expand Up @@ -42,34 +49,80 @@ def identifier(self) -> str:
return f"{self.__class__.__module__}:{self.__class__.__qualname__}"


class NoteApplication(ActionButton):
"""An Application that can be shown in a note."""
class ApplicationScope(StrEnum):
"""Available scopes for embedded applications."""

NAME: str = ""
IDENTIFIER: str = ""
NOTE = "note"

@property
def BUTTON_TITLE(self) -> str: # type: ignore[override]
"""Return NAME as the button title."""
return self.NAME

@property
def BUTTON_KEY(self) -> str: # type: ignore[override]
"""Return IDENTIFIER as the button key."""
return self.IDENTIFIER
class EmbeddedApplication(Application, ABC):
"""An embeddable application that can be registered to Canvas."""

NAME: str
SCOPE: ApplicationScope
IDENTIFIER: str | None = None
PRIORITY: int = 0

def compute(self) -> list[Effect]:
"""Handle the application events."""
match self.event.type:
case EventType.APPLICATION__ON_GET:
if self._matches_scope() and self.visible():
return [
ShowApplicationEffect(
name=self.NAME,
identifier=self.identifier,
open_by_default=self.open_by_default(),
priority=self.PRIORITY,
).apply()
]
return []
case _:
return super().compute()

def _matches_scope(self) -> bool:
"""Check if the event scope matches the application scope."""
return self.event.context.get("scope") == self.SCOPE

def open_by_default(self) -> bool:
"""Open the application by default."""
return False

def visible(self) -> bool:
"""Determine whether the application should be visible."""
return True

@property
def BUTTON_LOCATION(self) -> ActionButton.ButtonLocation: # type: ignore[override]
"""Return the note body as the button location."""
return ActionButton.ButtonLocation.NOTE_BODY
def identifier(self) -> str:
"""The application identifier."""
return self.IDENTIFIER if self.IDENTIFIER else super().identifier

@abstractmethod

class NoteApplication(EmbeddedApplication):
"""An Application that can be shown in a note."""

SCOPE = ApplicationScope.NOTE

def on_open(self) -> Effect | list[Effect]:
"""Delegate to handle() for backward compatibility with old plugins."""
# If a subclass overrides handle(), call it for backward compat.
# New plugins should override on_open() directly.
return self.handle()

@deprecation.deprecated(
deprecated_in="0.111.0",
removed_in="1.0.0",
current_version=version,
details="Use 'on_open' instead",
)
def handle(self) -> list[Effect]:
"""Method to handle button click."""
raise NotImplementedError("Implement to handle button click")
"""Method to handle application click/on_open."""
return []


__exports__ = (
"Application",
"ApplicationScope",
"EmbeddedApplication",
"NoteApplication",
)
Loading
Loading