From 341c34a97deb1e2f4613d9f6146383662e673c7d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pete=20An=C3=B8ther?= <develop.pit@gmail.com>
Date: Tue, 23 Jan 2024 14:08:29 -0300
Subject: [PATCH] EZQMS-527: Introduced `ActionButton` component (#4412)

EZQMS-527: Introduced `ActionButton` component

---------

Signed-off-by: Petr Vyazovetskiy <develop.pit@gmail.com>
---
 plugins/view-resources/src/actions.ts         | 68 +++++++++---------
 .../src/components/ActionButton.svelte        | 71 +++++++++++++++++++
 plugins/view-resources/src/index.ts           |  1 +
 3 files changed, 108 insertions(+), 32 deletions(-)
 create mode 100644 plugins/view-resources/src/components/ActionButton.svelte

diff --git a/plugins/view-resources/src/actions.ts b/plugins/view-resources/src/actions.ts
index f9551619f6..ce7fa036de 100644
--- a/plugins/view-resources/src/actions.ts
+++ b/plugins/view-resources/src/actions.ts
@@ -64,42 +64,13 @@ export async function getActions (
   derived: Ref<Class<Doc>> = core.class.Doc,
   mode: ViewContextType = 'context'
 ): Promise<Action[]> {
-  let actions: Action[] = await client.findAll(view.class.Action, {
+  const actions: Action[] = await client.findAll(view.class.Action, {
     'context.mode': mode
   })
 
+  const filteredActions = await filterAvailableActions(actions, client, doc, derived)
+
   const categories: Partial<Record<ActionGroup | 'top', number>> = { top: 1, tools: 50, other: 100, remove: 200 }
-
-  if (Array.isArray(doc)) {
-    for (const d of doc) {
-      actions = filterActions(client, d, actions, derived)
-    }
-  } else {
-    actions = filterActions(client, doc, actions, derived)
-  }
-  const inputVal: ViewActionInput[] = ['none']
-  if (!Array.isArray(doc) || doc.length === 1) {
-    inputVal.push('focus')
-    inputVal.push('any')
-  }
-  if (Array.isArray(doc) && doc.length > 0) {
-    inputVal.push('selection')
-    inputVal.push('any')
-  }
-  actions = actions.filter((it) => inputVal.includes(it.input))
-
-  const filteredActions: Action[] = []
-  for (const action of actions) {
-    if (action.visibilityTester == null) {
-      filteredActions.push(action)
-    } else {
-      const visibilityTester = await getResource(action.visibilityTester)
-
-      if (await visibilityTester(doc)) {
-        filteredActions.push(action)
-      }
-    }
-  }
   filteredActions.sort((a, b) => {
     const aTarget = categories[a.context.group ?? 'top'] ?? 0
     const bTarget = categories[b.context.group ?? 'top'] ?? 0
@@ -108,6 +79,39 @@ export async function getActions (
   return filteredActions
 }
 
+export async function filterAvailableActions (
+  actions: Action[],
+  client: Client,
+  doc: Doc | Doc[],
+  derived: Ref<Class<Doc>> = core.class.Doc
+): Promise<Action[]> {
+  actions = (Array.isArray(doc) ? doc : [doc]).reduce(
+    (actions, doc) => filterActions(client, doc, actions, derived),
+    actions
+  )
+
+  const input = (['none'] as ViewActionInput[])
+    .concat(Array.isArray(doc) && doc.length > 0 ? ['selection', 'any'] : [])
+    .concat(!Array.isArray(doc) || doc.length === 1 ? ['focus', 'any'] : [])
+  actions = actions.filter((it) => input.includes(it.input))
+
+  const result: Action[] = []
+  for (const action of actions) {
+    if (action.visibilityTester == null) {
+      result.push(action)
+    } else {
+      const visibilityTester = await getResource(action.visibilityTester)
+
+      if (await visibilityTester(doc)) {
+        result.push(action)
+      }
+    }
+  }
+
+  return result
+}
+
+/** @public */
 export async function invokeAction (
   object: Doc | Doc[],
   evt: Event,
diff --git a/plugins/view-resources/src/components/ActionButton.svelte b/plugins/view-resources/src/components/ActionButton.svelte
new file mode 100644
index 0000000000..0ae9cf0eea
--- /dev/null
+++ b/plugins/view-resources/src/components/ActionButton.svelte
@@ -0,0 +1,71 @@
+<!--
+// Copyright © 2022 Hardcore Engineering Inc.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the License.
+-->
+
+<script lang="ts">
+  import { ComponentProps } from 'svelte'
+  import { Button } from '@hcengineering/ui'
+  import { Doc, Ref } from '@hcengineering/core'
+  import { Action, ViewContextType } from '@hcengineering/view'
+  import { getClient } from '@hcengineering/presentation'
+  import { filterAvailableActions, invokeAction } from '../actions'
+  import view from '../plugin'
+
+  type $$Props = Omit<ComponentProps<Button>, 'icon' | 'label'> & {
+    id: Ref<Action>
+    object: Doc | Doc[]
+    mode?: ViewContextType
+  }
+  export let disabled: boolean = false
+  export let id: Ref<Action>
+  export let object: Doc | Doc[]
+  export let mode: ViewContextType | undefined = undefined
+
+  const client = getClient()
+
+  let action: Action | null = null
+  $: void client
+    .findOne(view.class.Action, {
+      'context.mode': mode,
+      _id: id
+    })
+    .then((result) => {
+      action = result ?? null
+    })
+
+  let isAvailable = false
+  $: if (action !== null) {
+    void filterAvailableActions([action], client, object).then((result) => {
+      isAvailable = result[0] === action
+    })
+  }
+
+  let isBeingInvoked = false
+</script>
+
+{#if action !== null && isAvailable}
+  <Button
+    {...$$props}
+    icon={action.icon}
+    label={action.label}
+    disabled={disabled || isBeingInvoked}
+    on:click={async (event) => {
+      if (action !== null) {
+        isBeingInvoked = true
+        await invokeAction(object, event, action.action, action.actionProps)
+        isBeingInvoked = false
+      }
+    }}
+  />
+{/if}
diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts
index 46ef6328fb..4543fd9df8 100644
--- a/plugins/view-resources/src/index.ts
+++ b/plugins/view-resources/src/index.ts
@@ -114,6 +114,7 @@ import { IndexedDocumentPreview } from '@hcengineering/presentation'
 import { showEmptyGroups } from './viewOptions'
 import { AggregationMiddleware } from './middleware'
 export { getActions, invokeAction, getContextActions } from './actions'
+export { default as ActionButton } from './components/ActionButton.svelte'
 export { default as ActionHandler } from './components/ActionHandler.svelte'
 export { default as FilterButton } from './components/filter/FilterButton.svelte'
 export { default as FixedColumn } from './components/FixedColumn.svelte'