1
0

WorkflowsLocal.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. <template>
  2. <div
  3. v-if="workflowStore.getWorkflows.length === 0"
  4. class="md:flex items-center md:text-left text-center py-12"
  5. >
  6. <img src="@/assets/svg/alien.svg" class="w-96" />
  7. <div class="ml-4">
  8. <h1 class="mb-6 max-w-md text-2xl font-semibold">
  9. {{ t('message.empty') }}
  10. </h1>
  11. </div>
  12. </div>
  13. <template v-else>
  14. <div v-if="pinnedWorkflows.length > 0" class="mb-8 border-b pb-8">
  15. <div class="flex items-center">
  16. <v-remixicon name="riPushpin2Line" class="mr-2" size="20" />
  17. <span>{{ t('workflow.pinWorkflow.pinned') }}</span>
  18. </div>
  19. <div class="workflows-container mt-4">
  20. <workflows-local-card
  21. v-for="workflow in pinnedWorkflows"
  22. :key="workflow.id"
  23. :workflow="workflow"
  24. :is-hosted="userStore.hostedWorkflows[workflow.id]"
  25. :is-shared="sharedWorkflowStore.getById(workflow.id)"
  26. :is-pinned="true"
  27. :menu="menu"
  28. @dragstart="onDragStart"
  29. @execute="executeWorkflow(workflow)"
  30. @toggle-pin="togglePinWorkflow(workflow)"
  31. @toggle-disable="toggleDisableWorkflow(workflow)"
  32. />
  33. </div>
  34. </div>
  35. <div class="workflows-container">
  36. <workflows-local-card
  37. v-for="workflow in workflows"
  38. :key="workflow.id"
  39. :workflow="workflow"
  40. :is-hosted="userStore.hostedWorkflows[workflow.id]"
  41. :is-shared="sharedWorkflowStore.getById(workflow.id)"
  42. :is-pinned="state.pinnedWorkflows.includes(workflow.id)"
  43. :menu="menu"
  44. @dragstart="onDragStart"
  45. @execute="executeWorkflow(workflow)"
  46. @toggle-pin="togglePinWorkflow(workflow)"
  47. @toggle-disable="toggleDisableWorkflow(workflow)"
  48. />
  49. </div>
  50. <div
  51. v-if="filteredWorkflows.length > 18"
  52. class="mt-8 flex items-center justify-between"
  53. >
  54. <div>
  55. {{ t('components.pagination.text1') }}
  56. <select
  57. :value="pagination.perPage"
  58. class="bg-input rounded-md p-1"
  59. @change="onPerPageChange"
  60. >
  61. <option v-for="num in [18, 32, 64, 128]" :key="num" :value="num">
  62. {{ num }}
  63. </option>
  64. </select>
  65. {{
  66. t('components.pagination.text2', {
  67. count: filteredWorkflows.length,
  68. })
  69. }}
  70. </div>
  71. <ui-pagination
  72. v-model="pagination.currentPage"
  73. :per-page="pagination.perPage"
  74. :records="filteredWorkflows.length"
  75. />
  76. </div>
  77. </template>
  78. <ui-modal v-model="renameState.show" title="Workflow">
  79. <ui-input
  80. v-model="renameState.name"
  81. :placeholder="t('common.name')"
  82. autofocus
  83. class="mb-4 w-full"
  84. @keyup.enter="renameWorkflow"
  85. />
  86. <ui-textarea
  87. v-model="renameState.description"
  88. :placeholder="t('common.description')"
  89. height="165px"
  90. class="w-full dark:text-gray-200"
  91. max="300"
  92. style="min-height: 140px"
  93. />
  94. <p class="mb-6 text-right text-gray-600 dark:text-gray-200">
  95. {{ renameState.description.length }}/300
  96. </p>
  97. <div class="flex space-x-2">
  98. <ui-button class="w-full" @click="clearRenameModal">
  99. {{ t('common.cancel') }}
  100. </ui-button>
  101. <ui-button variant="accent" class="w-full" @click="renameWorkflow">
  102. {{ t('common.update') }}
  103. </ui-button>
  104. </div>
  105. </ui-modal>
  106. </template>
  107. <script setup>
  108. import {
  109. shallowReactive,
  110. computed,
  111. onMounted,
  112. onBeforeUnmount,
  113. watch,
  114. } from 'vue';
  115. import { useI18n } from 'vue-i18n';
  116. import SelectionArea from '@viselect/vanilla';
  117. import browser from 'webextension-polyfill';
  118. import cloneDeep from 'lodash.clonedeep';
  119. import { arraySorter } from '@/utils/helper';
  120. import { useUserStore } from '@/stores/user';
  121. import { useDialog } from '@/composable/dialog';
  122. import { useWorkflowStore } from '@/stores/workflow';
  123. import { exportWorkflow } from '@/utils/workflowData';
  124. import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
  125. import { executeWorkflow } from '@/workflowEngine';
  126. import WorkflowsLocalCard from './WorkflowsLocalCard.vue';
  127. const props = defineProps({
  128. search: {
  129. type: String,
  130. default: '',
  131. },
  132. folderId: {
  133. type: String,
  134. default: '',
  135. },
  136. sort: {
  137. type: Object,
  138. default: () => ({
  139. by: '',
  140. order: '',
  141. }),
  142. },
  143. perPage: {
  144. type: Number,
  145. default: 18,
  146. },
  147. });
  148. const emit = defineEmits(['update:perPage']);
  149. const { t } = useI18n();
  150. const dialog = useDialog();
  151. const userStore = useUserStore();
  152. const workflowStore = useWorkflowStore();
  153. const sharedWorkflowStore = useSharedWorkflowStore();
  154. const state = shallowReactive({
  155. pinnedWorkflows: [],
  156. selectedWorkflows: [],
  157. });
  158. const renameState = shallowReactive({
  159. id: '',
  160. name: '',
  161. show: false,
  162. description: '',
  163. });
  164. const pagination = shallowReactive({
  165. currentPage: 1,
  166. perPage: +`${props.perPage}` || 18,
  167. });
  168. const selection = new SelectionArea({
  169. container: '.workflows-list',
  170. startareas: ['.workflows-list'],
  171. boundaries: ['.workflows-list'],
  172. selectables: ['.local-workflow'],
  173. });
  174. selection
  175. .on('beforestart', ({ event }) => {
  176. return (
  177. event.target.tagName !== 'INPUT' &&
  178. !event.target.closest('.local-workflow')
  179. );
  180. })
  181. .on('start', () => {
  182. /* eslint-disable-next-line */
  183. clearSelectedWorkflows();
  184. })
  185. .on('move', (event) => {
  186. event.store.changed.added.forEach((el) => {
  187. el.classList.add('ring-2');
  188. });
  189. event.store.changed.removed.forEach((el) => {
  190. el.classList.remove('ring-2');
  191. });
  192. })
  193. .on('stop', (event) => {
  194. state.selectedWorkflows = event.store.selected.map(
  195. (el) => el.dataset?.workflow
  196. );
  197. });
  198. const filteredWorkflows = computed(() => {
  199. const filtered = workflowStore.getWorkflows.filter(
  200. ({ name, folderId }) =>
  201. name.toLocaleLowerCase().includes(props.search.toLocaleLowerCase()) &&
  202. (!props.folderId || props.folderId === folderId)
  203. );
  204. return arraySorter({
  205. data: filtered,
  206. key: props.sort.by,
  207. order: props.sort.order,
  208. });
  209. });
  210. const workflows = computed(() =>
  211. filteredWorkflows.value.slice(
  212. (pagination.currentPage - 1) * pagination.perPage,
  213. pagination.currentPage * pagination.perPage
  214. )
  215. );
  216. const pinnedWorkflows = computed(() => {
  217. const list = [];
  218. state.pinnedWorkflows.forEach((workflowId) => {
  219. const workflow = workflowStore.getById(workflowId);
  220. if (
  221. !workflow ||
  222. !workflow.name
  223. .toLocaleLowerCase()
  224. .includes(props.search.toLocaleLowerCase())
  225. )
  226. return;
  227. list.push(workflow);
  228. });
  229. return arraySorter({
  230. data: list,
  231. key: props.sort.by,
  232. order: props.sort.order,
  233. });
  234. });
  235. function onPerPageChange(event) {
  236. const { value } = event.target;
  237. pagination.perPage = +value;
  238. emit('update:perPage', +value);
  239. }
  240. function toggleDisableWorkflow({ id, isDisabled }) {
  241. workflowStore.update({
  242. id,
  243. data: {
  244. isDisabled: !isDisabled,
  245. },
  246. });
  247. }
  248. function clearRenameModal() {
  249. Object.assign(renameState, {
  250. id: '',
  251. name: '',
  252. show: false,
  253. description: '',
  254. });
  255. }
  256. function initRenameWorkflow({ name, description, id }) {
  257. Object.assign(renameState, {
  258. id,
  259. name,
  260. show: true,
  261. description,
  262. });
  263. }
  264. function renameWorkflow() {
  265. workflowStore.update({
  266. id: renameState.id,
  267. data: {
  268. name: renameState.name,
  269. description: renameState.description,
  270. },
  271. });
  272. clearRenameModal();
  273. }
  274. function deleteWorkflow({ name, id }) {
  275. dialog.confirm({
  276. title: t('workflow.delete'),
  277. okVariant: 'danger',
  278. body: t('message.delete', { name }),
  279. onConfirm: () => {
  280. workflowStore.delete(id);
  281. },
  282. });
  283. }
  284. function deleteSelectedWorkflows({ target, key }) {
  285. const excludeTags = ['INPUT', 'TEXTAREA', 'SELECT'];
  286. if (
  287. excludeTags.includes(target.tagName) ||
  288. key !== 'Delete' ||
  289. state.selectedWorkflows.length === 0
  290. )
  291. return;
  292. if (state.selectedWorkflows.length === 1) {
  293. const [workflowId] = state.selectedWorkflows;
  294. const workflow = workflowStore.getById(workflowId);
  295. deleteWorkflow(workflow);
  296. } else {
  297. dialog.confirm({
  298. title: t('workflow.delete'),
  299. okVariant: 'danger',
  300. body: t('message.delete', {
  301. name: `${state.selectedWorkflows.length} workflows`,
  302. }),
  303. onConfirm: async () => {
  304. await workflowStore.delete(state.selectedWorkflows);
  305. },
  306. });
  307. }
  308. }
  309. function duplicateWorkflow(workflow) {
  310. const clonedWorkflow = cloneDeep(workflow);
  311. const delKeys = ['$id', 'data', 'id', 'isDisabled'];
  312. delKeys.forEach((key) => {
  313. delete clonedWorkflow[key];
  314. });
  315. clonedWorkflow.createdAt = Date.now();
  316. clonedWorkflow.name += ' - copy';
  317. workflowStore.insert(clonedWorkflow);
  318. }
  319. function onDragStart({ dataTransfer, target }) {
  320. const payload = [...state.selectedWorkflows];
  321. const targetId = target.dataset?.workflow;
  322. if (targetId && !payload.includes(targetId)) payload.push(targetId);
  323. dataTransfer.setData('workflows', JSON.stringify(payload));
  324. }
  325. function clearSelectedWorkflows() {
  326. state.selectedWorkflows = [];
  327. selection.getSelection().forEach((el) => {
  328. el.classList.remove('ring-2');
  329. });
  330. selection.clearSelection();
  331. }
  332. function togglePinWorkflow(workflow) {
  333. const index = state.pinnedWorkflows.indexOf(workflow.id);
  334. const copyData = [...state.pinnedWorkflows];
  335. if (index === -1) {
  336. copyData.push(workflow.id);
  337. } else {
  338. copyData.splice(index, 1);
  339. }
  340. state.pinnedWorkflows = copyData;
  341. browser.storage.local.set({
  342. pinnedWorkflows: copyData,
  343. });
  344. }
  345. const menu = [
  346. {
  347. id: 'copy-id',
  348. name: 'Copy workflow id',
  349. icon: 'riFileCopyLine',
  350. action: (workflow) => {
  351. navigator.clipboard.writeText(workflow.id).catch((error) => {
  352. console.error(error);
  353. const textarea = document.createElement('textarea');
  354. textarea.value = workflow.id;
  355. textarea.select();
  356. document.execCommand('copy');
  357. textarea.blur();
  358. });
  359. },
  360. },
  361. {
  362. id: 'duplicate',
  363. name: t('common.duplicate'),
  364. icon: 'riFileCopyLine',
  365. action: duplicateWorkflow,
  366. },
  367. {
  368. id: 'export',
  369. name: t('common.export'),
  370. icon: 'riDownloadLine',
  371. action: exportWorkflow,
  372. },
  373. {
  374. id: 'rename',
  375. name: t('common.rename'),
  376. icon: 'riPencilLine',
  377. action: initRenameWorkflow,
  378. },
  379. {
  380. id: 'delete',
  381. name: t('common.delete'),
  382. icon: 'riDeleteBin7Line',
  383. action: deleteWorkflow,
  384. },
  385. ];
  386. watch(
  387. () => props.folderId,
  388. () => {
  389. pagination.currentPage = 1;
  390. }
  391. );
  392. onMounted(() => {
  393. window.addEventListener('keydown', deleteSelectedWorkflows);
  394. browser.storage.local.get('pinnedWorkflows').then((storage) => {
  395. state.pinnedWorkflows = storage.pinnedWorkflows || [];
  396. });
  397. });
  398. onBeforeUnmount(() => {
  399. window.removeEventListener('keydown', deleteSelectedWorkflows);
  400. });
  401. </script>