Workflows.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. <template>
  2. <div class="container pt-8 pb-4">
  3. <h1 class="text-2xl font-semibold capitalize">
  4. {{ t('common.workflow', 2) }}
  5. </h1>
  6. <div class="flex items-start mt-8">
  7. <div class="w-60 sticky top-8">
  8. <div class="flex w-full">
  9. <ui-button
  10. :title="shortcut['action:new'].readable"
  11. variant="accent"
  12. class="border-r rounded-r-none flex-1 font-semibold"
  13. @click="addWorkflowModal.show = true"
  14. >
  15. {{ t('workflow.new') }}
  16. </ui-button>
  17. <ui-popover>
  18. <template #trigger>
  19. <ui-button icon class="rounded-l-none" variant="accent">
  20. <v-remixicon name="riArrowLeftSLine" rotate="-90" />
  21. </ui-button>
  22. </template>
  23. <ui-list class="space-y-1">
  24. <ui-list-item
  25. v-close-popover
  26. class="cursor-pointer"
  27. @click="openImportDialog"
  28. >
  29. {{ t('workflow.import') }}
  30. </ui-list-item>
  31. <ui-list-item
  32. v-close-popover
  33. class="cursor-pointer"
  34. @click="addHostedWorkflow"
  35. >
  36. {{ t('workflow.host.add') }}
  37. </ui-list-item>
  38. </ui-list>
  39. </ui-popover>
  40. </div>
  41. <ui-list class="mt-6 space-y-2">
  42. <ui-list-item
  43. tag="a"
  44. href="https://www.automa.site/workflows"
  45. target="_blank"
  46. >
  47. <v-remixicon name="riCompass3Line" />
  48. <span class="ml-4 capitalize">
  49. {{ t('workflow.browse') }}
  50. </span>
  51. </ui-list-item>
  52. <ui-expand
  53. v-if="state.teams.length > 0"
  54. append-icon
  55. header-class="px-4 py-2 rounded-lg mb-1 hoverable w-full flex items-center"
  56. >
  57. <template #header>
  58. <v-remixicon name="riTeamLine" />
  59. <span class="ml-4 capitalize flex-1 text-left">
  60. Team Workflows
  61. </span>
  62. </template>
  63. <ui-list class="space-y-1">
  64. <ui-list-item
  65. v-for="team in state.teams"
  66. :key="team.id"
  67. :active="state.teamId === team.id || +state.teamId === team.id"
  68. :title="team.name"
  69. color="bg-box-transparent font-semibold"
  70. class="pl-14 cursor-pointer"
  71. @click="updateActiveTab({ activeTab: 'team', teamId: team.id })"
  72. >
  73. <span class="text-overflow">
  74. {{ team.name }}
  75. </span>
  76. </ui-list-item>
  77. </ui-list>
  78. </ui-expand>
  79. <ui-expand
  80. :model-value="true"
  81. append-icon
  82. header-class="px-4 py-2 rounded-lg hoverable w-full flex items-center"
  83. >
  84. <template #header>
  85. <v-remixicon name="riFlowChart" />
  86. <span class="ml-4 capitalize flex-1 text-left">
  87. {{ t('workflow.my') }}
  88. </span>
  89. </template>
  90. <ui-list class="space-y-1 mt-1">
  91. <ui-list-item
  92. tag="button"
  93. :active="state.activeTab === 'local'"
  94. color="bg-box-transparent font-semibold"
  95. class="pl-14"
  96. @click="updateActiveTab({ activeTab: 'local' })"
  97. >
  98. <span class="capitalize">
  99. {{ t('workflow.type.local') }}
  100. </span>
  101. </ui-list-item>
  102. <ui-list-item
  103. v-if="userStore.user"
  104. :active="state.activeTab === 'shared'"
  105. tag="button"
  106. color="bg-box-transparent font-semibold"
  107. class="pl-14"
  108. @click="updateActiveTab({ activeTab: 'shared' })"
  109. >
  110. <span class="capitalize">
  111. {{ t('workflow.type.shared') }}
  112. </span>
  113. </ui-list-item>
  114. <ui-list-item
  115. v-if="hostedWorkflows?.length > 0"
  116. :active="state.activeTab === 'host'"
  117. color="bg-box-transparent font-semibold"
  118. tag="button"
  119. class="pl-14"
  120. @click="updateActiveTab({ activeTab: 'host' })"
  121. >
  122. <span class="capitalize">
  123. {{ t('workflow.type.host') }}
  124. </span>
  125. </ui-list-item>
  126. </ui-list>
  127. </ui-expand>
  128. </ui-list>
  129. <workflows-folder
  130. v-if="state.activeTab === 'local'"
  131. v-model="state.activeFolder"
  132. />
  133. </div>
  134. <div
  135. class="flex-1 workflows-list ml-8"
  136. style="min-height: calc(100vh - 8rem)"
  137. @dblclick="clearSelectedWorkflows"
  138. >
  139. <div class="flex items-center">
  140. <ui-input
  141. id="search-input"
  142. v-model="state.query"
  143. :placeholder="`${t(`common.search`)}... (${
  144. shortcut['action:search'].readable
  145. })`"
  146. prepend-icon="riSearch2Line"
  147. />
  148. <div class="flex-grow"></div>
  149. <span v-tooltip:bottom.group="t('workflow.backupCloud')" class="mr-4">
  150. <ui-button tag="router-link" to="/backup" class="inline-block" icon>
  151. <v-remixicon name="riUploadCloud2Line" />
  152. </ui-button>
  153. </span>
  154. <div class="flex items-center workflow-sort">
  155. <ui-button
  156. icon
  157. class="rounded-r-none border-gray-300 dark:border-gray-700 border-r"
  158. @click="
  159. state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc'
  160. "
  161. >
  162. <v-remixicon
  163. :name="state.sortOrder === 'asc' ? 'riSortAsc' : 'riSortDesc'"
  164. />
  165. </ui-button>
  166. <ui-select v-model="state.sortBy" :placeholder="t('sort.sortBy')">
  167. <option v-for="sort in sorts" :key="sort" :value="sort">
  168. {{ t(`sort.${sort}`) }}
  169. </option>
  170. </ui-select>
  171. </div>
  172. </div>
  173. <ui-tab-panels v-model="state.activeTab" class="flex-1 mt-6">
  174. <ui-tab-panel value="team" cache>
  175. <workflows-user-team
  176. :active="state.activeTab === 'team'"
  177. :team-id="state.teamId"
  178. :search="state.query"
  179. :sort="{ by: state.sortBy, order: state.sortOrder }"
  180. />
  181. </ui-tab-panel>
  182. <ui-tab-panel value="shared" class="workflows-container">
  183. <workflows-shared
  184. :search="state.query"
  185. :sort="{ by: state.sortBy, order: state.sortOrder }"
  186. />
  187. </ui-tab-panel>
  188. <ui-tab-panel value="host" class="workflows-container">
  189. <workflows-hosted
  190. :search="state.query"
  191. :sort="{ by: state.sortBy, order: state.sortOrder }"
  192. />
  193. </ui-tab-panel>
  194. <ui-tab-panel value="local">
  195. <workflows-local
  196. :search="state.query"
  197. :per-page="state.perPage"
  198. :folder-id="state.activeFolder"
  199. :sort="{ by: state.sortBy, order: state.sortOrder }"
  200. />
  201. </ui-tab-panel>
  202. </ui-tab-panels>
  203. </div>
  204. </div>
  205. <ui-modal v-model="addWorkflowModal.show" title="Workflow">
  206. <ui-input
  207. v-model="addWorkflowModal.name"
  208. :placeholder="t('common.name')"
  209. autofocus
  210. class="w-full mb-4"
  211. @keyup.enter="addWorkflow"
  212. />
  213. <ui-textarea
  214. v-model="addWorkflowModal.description"
  215. :placeholder="t('common.description')"
  216. height="165px"
  217. class="w-full dark:text-gray-200"
  218. max="300"
  219. />
  220. <p class="mb-6 text-right text-gray-600 dark:text-gray-200">
  221. {{ addWorkflowModal.description.length }}/300
  222. </p>
  223. <div class="space-x-2 flex">
  224. <ui-button class="w-full" @click="clearAddWorkflowModal">
  225. {{ t('common.cancel') }}
  226. </ui-button>
  227. <ui-button variant="accent" class="w-full" @click="addWorkflow">
  228. {{ t('common.add') }}
  229. </ui-button>
  230. </div>
  231. </ui-modal>
  232. <shared-permissions-modal
  233. v-model="permissionState.showModal"
  234. :permissions="permissionState.items"
  235. />
  236. </div>
  237. </template>
  238. <script setup>
  239. import { computed, shallowReactive, watch, onMounted } from 'vue';
  240. import { useI18n } from 'vue-i18n';
  241. import { useRouter } from 'vue-router';
  242. import { useToast } from 'vue-toastification';
  243. import { useDialog } from '@/composable/dialog';
  244. import { useShortcut } from '@/composable/shortcut';
  245. import { useGroupTooltip } from '@/composable/groupTooltip';
  246. import { isWhitespace } from '@/utils/helper';
  247. import { useUserStore } from '@/stores/user';
  248. import { useWorkflowStore } from '@/stores/workflow';
  249. import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
  250. import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
  251. import { importWorkflow, getWorkflowPermissions } from '@/utils/workflowData';
  252. import WorkflowsLocal from '@/components/newtab/workflows/WorkflowsLocal.vue';
  253. import WorkflowsShared from '@/components/newtab/workflows/WorkflowsShared.vue';
  254. import WorkflowsHosted from '@/components/newtab/workflows/WorkflowsHosted.vue';
  255. import WorkflowsFolder from '@/components/newtab/workflows/WorkflowsFolder.vue';
  256. import WorkflowsUserTeam from '@/components/newtab/workflows/WorkflowsUserTeam.vue';
  257. import SharedPermissionsModal from '@/components/newtab/shared/SharedPermissionsModal.vue';
  258. useGroupTooltip();
  259. const { t } = useI18n();
  260. const toast = useToast();
  261. const dialog = useDialog();
  262. const router = useRouter();
  263. const userStore = useUserStore();
  264. const workflowStore = useWorkflowStore();
  265. const teamWorkflowStore = useTeamWorkflowStore();
  266. const hostedWorkflowStore = useHostedWorkflowStore();
  267. const sorts = ['name', 'createdAt'];
  268. const { teamId, active } = router.currentRoute.value.query;
  269. const savedSorts = JSON.parse(localStorage.getItem('workflow-sorts') || '{}');
  270. const validTeamId = userStore.user?.teams?.some(
  271. ({ id }) => id === teamId || id === +teamId
  272. );
  273. const state = shallowReactive({
  274. teams: [],
  275. query: '',
  276. activeFolder: '',
  277. activeTab: active || 'local',
  278. teamId: validTeamId ? teamId : '',
  279. perPage: savedSorts.perPage || 18,
  280. sortBy: savedSorts.sortBy || 'createdAt',
  281. sortOrder: savedSorts.sortOrder || 'desc',
  282. });
  283. const addWorkflowModal = shallowReactive({
  284. name: '',
  285. show: false,
  286. description: '',
  287. });
  288. const permissionState = shallowReactive({
  289. items: [],
  290. showModal: false,
  291. });
  292. const hostedWorkflows = computed(() => hostedWorkflowStore.toArray);
  293. function clearAddWorkflowModal() {
  294. Object.assign(addWorkflowModal, {
  295. name: '',
  296. show: false,
  297. description: '',
  298. });
  299. }
  300. function updateActiveTab(data = {}) {
  301. if (data.activeTab !== 'team') data.teamId = '';
  302. Object.assign(state, data);
  303. }
  304. function addWorkflow() {
  305. workflowStore.insert({
  306. name: addWorkflowModal.name,
  307. description: addWorkflowModal.description,
  308. });
  309. clearAddWorkflowModal();
  310. }
  311. function addHostedWorkflow() {
  312. dialog.prompt({
  313. async: true,
  314. inputType: 'url',
  315. okText: t('common.add'),
  316. title: t('workflow.host.add'),
  317. label: t('workflow.host.id'),
  318. placeholder: 'abcd123',
  319. onConfirm: async (value) => {
  320. if (isWhitespace(value)) return false;
  321. const hostId = value.replace(/\s/g, '');
  322. try {
  323. await hostedWorkflowStore.addHostedWorkflow(hostId);
  324. return true;
  325. } catch (error) {
  326. const messages = {
  327. exists: t('workflow.host.messages.hostExist'),
  328. 'rate-exceeded': t('message.rateExceeded'),
  329. 'not-found': t('workflow.host.messages.notFound', { id: hostId }),
  330. };
  331. const errorMessage = messages[error.message] || error.message;
  332. toast.error(errorMessage);
  333. return false;
  334. }
  335. },
  336. });
  337. }
  338. async function openImportDialog() {
  339. try {
  340. const workflows = await importWorkflow({ multiple: true });
  341. const insertedWorkflows = Object.values(workflows);
  342. let requiredPermissions = [];
  343. for (const workflow of insertedWorkflows) {
  344. if (workflow.drawflow) {
  345. const permissions = await getWorkflowPermissions(workflow.drawflow);
  346. requiredPermissions.push(...permissions);
  347. }
  348. }
  349. requiredPermissions = Array.from(new Set(requiredPermissions));
  350. if (requiredPermissions.length === 0) return;
  351. permissionState.items = requiredPermissions;
  352. permissionState.showModal = true;
  353. } catch (error) {
  354. console.error(error);
  355. }
  356. }
  357. const shortcut = useShortcut(['action:search', 'action:new'], ({ id }) => {
  358. if (id === 'action:search') {
  359. const searchInput = document.querySelector('#search-input input');
  360. searchInput?.focus();
  361. } else {
  362. addWorkflowModal.show = true;
  363. }
  364. });
  365. watch(
  366. () => [state.sortOrder, state.sortBy, state.perPage],
  367. ([sortOrder, sortBy, perPage]) => {
  368. localStorage.setItem(
  369. 'workflow-sorts',
  370. JSON.stringify({ sortOrder, sortBy, perPage })
  371. );
  372. }
  373. );
  374. watch(
  375. () => [state.activeTab, state.teamId],
  376. ([activeTab, teamIdQuery]) => {
  377. const query = { active: activeTab };
  378. if (teamIdQuery) query.teamId = teamIdQuery;
  379. router.replace({ ...router.currentRoute.value, query });
  380. }
  381. );
  382. onMounted(() => {
  383. const teams = [];
  384. let unknownInputted = false;
  385. Object.keys(teamWorkflowStore.workflows).forEach((id) => {
  386. const userTeam = userStore.user?.teams?.find(
  387. (team) => team.id === id || team.id === +id
  388. );
  389. if (userTeam) {
  390. teams.push({ name: userTeam.name, id: userTeam.id });
  391. } else if (!unknownInputted) {
  392. unknownInputted = true;
  393. teams.unshift({ name: '(unknown)', id: '(unknown)' });
  394. }
  395. });
  396. state.teams = teams;
  397. });
  398. </script>
  399. <style>
  400. .workflow-sort select {
  401. @apply rounded-l-none !important;
  402. }
  403. .workflows-container {
  404. @apply grid gap-4 grid-cols-3 2xl:grid-cols-4;
  405. }
  406. </style>