From f7edf1839172aa30d4b5228aba0cdcab9a064248 Mon Sep 17 00:00:00 2001 From: 3wc <3wc@doesthisthing.work> Date: Sun, 29 Oct 2023 21:49:26 +0000 Subject: [PATCH] Fuckkk yeah, working mapping-adding --- hamstertools/app.py | 214 ++++++++++++++++++++++++++++++++++++++++-- hamstertools/app.tcss | 31 ++++++ 2 files changed, 236 insertions(+), 9 deletions(-) diff --git a/hamstertools/app.py b/hamstertools/app.py index 4560800..a8fe020 100644 --- a/hamstertools/app.py +++ b/hamstertools/app.py @@ -1,9 +1,15 @@ +from textual import on from textual.app import App, ComposeResult from textual.binding import Binding -from textual.widgets import Header, Footer, DataTable, Input +from textual.containers import Grid +from textual.events import DescendantBlur +from textual.widgets import Header, Footer, DataTable, Input, Button, Label, Checkbox from textual.containers import Horizontal, Vertical from textual.coordinate import Coordinate -from textual.screen import Screen +from textual.reactive import reactive +from textual.screen import Screen, ModalScreen + +from textual_autocomplete import AutoComplete, Dropdown, DropdownItem from peewee import fn, JOIN @@ -66,13 +72,174 @@ class ListScreen(Screen): self._refresh(event.value) -class ActivitiesScreen(ListScreen): +class ActivityEditScreen(ModalScreen): + BINDINGS = [ + ("escape", "cancel", "Cancel") + ] + + def compose(self) -> ComposeResult: + yield Grid( + Label("Are you sure you want to quit?", id="question"), + Button("Quit", variant="error", id="quit"), + Button("Cancel", variant="primary", id="cancel"), + id="dialog", + ) + + def action_cancel(self): + self.dismiss(None) + + +class ActivityMappingScreen(ModalScreen): + BINDINGS = [ + ("ctrl+g", "global", "Toggle global"), + ("ctrl+s", "save", "Save"), + ("escape", "cancel", "Cancel") + ] + + customer_id = None + project_id = None + activity_id = None + + def __init__(self, category, activity): + self.category = category + self.activity = activity + super().__init__() + + @staticmethod + def _filter_dropdowns(options, value): + matches = [c for c in options if value.lower() in c.main.plain.lower()] + return sorted(matches, key=lambda v: v.main.plain.startswith(value.lower())) + + def _get_customers(self, input_state): + customers = [ + DropdownItem(c.name, str(c.id)) + for c in KimaiCustomer.select() + ] + return ActivityMappingScreen._filter_dropdowns(customers, + input_state.value) + + def _get_projects(self, input_state): + projects = [ + DropdownItem(p.name, str(p.id)) + for p in KimaiProject.select().where( + KimaiProject.customer_id == self.customer_id + ) + ] + return ActivityMappingScreen._filter_dropdowns(projects, + input_state.value) + + def _get_activities(self, input_state): + activities = KimaiActivity.select() + + if self.query_one('#global').value: + activities = activities.where( + KimaiActivity.project_id.is_null(), + ) + else: + activities = activities.where( + KimaiActivity.project_id == self.project_id + ) + + return ActivityMappingScreen._filter_dropdowns([ + DropdownItem(a.name, str(a.id)) + for a in activities], input_state.value) + + def compose(self) -> ComposeResult: + yield Vertical( + Horizontal( + Label(f"Mapping for {self.activity}@{self.category}"), + ), + Horizontal( + Label("Customer"), + AutoComplete( + Input(placeholder="Type to search...", id="customer"), + Dropdown(items=self._get_customers), + ) + ), + Horizontal( + Label("Project"), + AutoComplete( + Input(placeholder="Type to search...", id='project'), + Dropdown(items=self._get_projects), + ) + ), + Horizontal( + Label("Activity"), + AutoComplete( + Input(placeholder="Type to search...", id='activity'), + Dropdown(items=self._get_activities), + ) + ), + Horizontal( + Label("Description"), + Input(id='description'), + ), + Horizontal( + Label("Tags"), + Input(id='tags'), + ), + Horizontal(Checkbox("Global", id='global')), + ) + + @on(Input.Submitted, '#customer') + def customer_submitted(self, event): + if event.control.parent.dropdown.selected_item is not None: + self.customer_id = str(event.control.parent.dropdown.selected_item.left_meta) + self.query_one('#project').focus() + + @on(DescendantBlur, '#customer') + def customer_blur(self, event): + if event.control.parent.dropdown.selected_item is not None: + self.customer_id = str(event.control.parent.dropdown.selected_item.left_meta) + + @on(Input.Submitted, '#project') + def project_submitted(self, event): + if event.control.parent.dropdown.selected_item is not None: + self.project_id = str(event.control.parent.dropdown.selected_item.left_meta) + self.query_one('#activity').focus() + + @on(DescendantBlur, '#project') + def project_blur(self, event): + if event.control.parent.dropdown.selected_item is not None: + self.project_id = str(event.control.parent.dropdown.selected_item.left_meta) + + @on(Input.Submitted, '#activity') + def activity_submitted(self, event): + if event.control.parent.dropdown.selected_item is not None: + self.activity_id = str(event.control.parent.dropdown.selected_item.left_meta) + self.query_one('#activity').focus() + + @on(DescendantBlur, '#activity') + def activity_blur(self, event): + if event.control.parent.dropdown.selected_item is not None: + self.activity_id = str(event.control.parent.dropdown.selected_item.left_meta) + + def action_global(self): + self.query_one('#global').value = not self.query_one('#global').value + + def action_save(self): + self.dismiss({ + 'kimai_customer_id': self.customer_id, + 'kimai_project_id': self.project_id, + 'kimai_activity_id': self.activity_id, + 'kimai_description': self.query_one('#description').value, + 'kimai_tags': self.query_one('#tags').value, + 'global': self.query_one('#global').value, + }) + + def action_cancel(self): + self.dismiss(None) + + +class ActivityListScreen(ListScreen): BINDINGS = [ ("s", "sort", "Sort"), ("r", "refresh", "Refresh"), ("/", "filter", "Search"), - ("d", "delete", "Delete activity"), + ("d", "delete", "Delete"), ("f", "move_facts", "Move facts"), + ("e", "edit", "Edit"), + ("m", "mapping", "Mapping"), Binding(key="escape", action="cancelfilter", show=False), ] @@ -191,8 +358,37 @@ class ActivitiesScreen(ListScreen): self._refresh(filter_input.value) del self.move_from_activity + def action_edit(self): + def handle_edit(properties): + print(properties) -class CategoriesScreen(ListScreen): + self.app.push_screen(ActivityEditScreen(), handle_edit) + + def action_mapping(self): + selected_activity = HamsterActivity.select( + HamsterActivity, + fn.COALESCE(HamsterCategory.name, "None").alias("category_name"), + ).join(HamsterCategory, JOIN.LEFT_OUTER).where( + HamsterActivity.id == self.table.get_cell_at( + Coordinate(self.table.cursor_coordinate.row, 2), + ) + ).get() + + def handle_mapping(mapping): + if mapping is None: + return + m = HamsterKimaiMapping.create(hamster_activity=selected_activity, **mapping) + m.save() + filter_input = self.query_one("#filter") + self._refresh(filter_input.value) + + self.app.push_screen(ActivityMappingScreen( + category=selected_activity.category_name, + activity=selected_activity.name + ), handle_mapping) + + +class CategoryListScreen(ListScreen): BINDINGS = [ ("s", "sort", "Sort"), ("r", "refresh", "Refresh"), @@ -249,7 +445,7 @@ class CategoriesScreen(ListScreen): self.table.remove_row(row_key) -class KimaiScreen(ListScreen): +class KimaiProjectListScreen(ListScreen): BINDINGS = [ ("s", "sort", "Sort"), ("r", "refresh", "Refresh"), @@ -362,9 +558,9 @@ class HamsterToolsApp(App): db.init("hamster-testing.db") self.MODES = { - "categories": CategoriesScreen(), - "activities": ActivitiesScreen(), - "kimai": KimaiScreen(), + "categories": CategoryListScreen(), + "activities": ActivityListScreen(), + "kimai": KimaiProjectListScreen(), } super().__init__() diff --git a/hamstertools/app.tcss b/hamstertools/app.tcss index 0f6cab6..e6e8785 100644 --- a/hamstertools/app.tcss +++ b/hamstertools/app.tcss @@ -13,3 +13,34 @@ DataTable:focus .datatable--cursor { #filter { display: none; } + +ActivityEditScreen, ActivityMappingScreen { + align: center middle; +} + +ActivityMappingScreen > Vertical { + padding: 0 1; + width: auto; + height: 30; + border: thick $background 80%; + background: $surface; +} + +ActivityMappingScreen Horizontal { + align: left middle; + width: auto; +} + +ActivityMappingScreen Label { + padding: 0 1; + width: auto; + border: blank; +} + +ActivityMappingScreen AutoComplete { + width: 80; +} + +#description, #tags { + width: 30; +}