WorkflowDetailsCard.vue 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. <template>
  2. <div class="px-4 flex items-start mb-2 mt-1">
  3. <ui-popover class="mr-2 h-8">
  4. <template #trigger>
  5. <span
  6. :title="t('workflow.sidebar.workflowIcon')"
  7. class="cursor-pointer inline-block h-full"
  8. >
  9. <ui-img
  10. v-if="workflow.icon.startsWith('http')"
  11. :src="workflow.icon"
  12. class="w-8 h-8"
  13. />
  14. <v-remixicon v-else :name="workflow.icon" size="26" class="mt-1" />
  15. </span>
  16. </template>
  17. <div class="w-56">
  18. <p class="mb-2">{{ t('workflow.sidebar.workflowIcon') }}</p>
  19. <div class="grid grid-cols-5 mb-2 gap-1">
  20. <span
  21. v-for="icon in icons"
  22. :key="icon"
  23. class="cursor-pointer rounded-lg inline-block text-center p-2 hoverable"
  24. @click="$emit('update', { icon })"
  25. >
  26. <v-remixicon :name="icon" />
  27. </span>
  28. </div>
  29. <ui-input
  30. :model-value="workflow.icon.startsWith('http') ? workflow.icon : ''"
  31. type="url"
  32. placeholder="http://example.com/img.png"
  33. label="Icon URL"
  34. @change="updateWorkflowIcon"
  35. />
  36. </div>
  37. </ui-popover>
  38. <div class="flex-1 overflow-hidden">
  39. <p class="font-semibold mt-1 text-overflow text-lg leading-tight">
  40. {{ workflow.name }}
  41. </p>
  42. <p
  43. class="leading-tight cursor-pointer"
  44. :class="descriptionCollapsed ? 'line-clamp' : 'whitespace-pre-wrap'"
  45. @click="descriptionCollapsed = !descriptionCollapsed"
  46. >
  47. {{ workflow.description }}
  48. </p>
  49. </div>
  50. </div>
  51. <ui-input
  52. id="search-input"
  53. v-model="query"
  54. :placeholder="`${t('common.search')}... (${
  55. shortcut['action:search'].readable
  56. })`"
  57. prepend-icon="riSearch2Line"
  58. class="px-4 mt-4 mb-2 w-full"
  59. />
  60. <div class="scroll bg-scroll px-4 flex-1 relative overflow-auto">
  61. <workflow-block-list
  62. v-if="pinnedBlocksList.length > 0"
  63. :model-value="true"
  64. :blocks="pinnedBlocksList"
  65. :category="pinnedCategory"
  66. :pinned="pinnedBlocks"
  67. @pin="pinBlock"
  68. />
  69. <workflow-block-list
  70. v-for="(items, catId) in blocks"
  71. :key="catId"
  72. :model-value="true"
  73. :blocks="items"
  74. :category="categories[catId]"
  75. :pinned="pinnedBlocks"
  76. @pin="pinBlock"
  77. />
  78. </div>
  79. </template>
  80. <script setup>
  81. import { computed, ref, onMounted, watch, toRaw } from 'vue';
  82. import { useI18n } from 'vue-i18n';
  83. import browser from 'webextension-polyfill';
  84. import { useShortcut } from '@/composable/shortcut';
  85. import { categories } from '@/utils/shared';
  86. import { getBlocks } from '@/utils/getSharedData';
  87. import WorkflowBlockList from './WorkflowBlockList.vue';
  88. defineProps({
  89. workflow: {
  90. type: Object,
  91. default: () => ({}),
  92. },
  93. dataChanged: {
  94. type: Boolean,
  95. default: false,
  96. },
  97. });
  98. const emit = defineEmits(['update']);
  99. const { t, te } = useI18n();
  100. const shortcut = useShortcut('action:search', () => {
  101. const searchInput = document.querySelector('#search-input input');
  102. searchInput?.focus();
  103. });
  104. const pinnedCategory = {
  105. name: 'Pinned blocks',
  106. color: 'bg-accent',
  107. };
  108. const icons = [
  109. 'mdiPackageVariantClosed',
  110. 'riGlobalLine',
  111. 'riFileTextLine',
  112. 'riEqualizerLine',
  113. 'riTimerLine',
  114. 'riCalendarLine',
  115. 'riFlashlightLine',
  116. 'riLightbulbFlashLine',
  117. 'riDatabase2Line',
  118. 'riWindowLine',
  119. 'riCursorLine',
  120. 'riDownloadLine',
  121. 'riCommandLine',
  122. ];
  123. const copyBlocks = getBlocks();
  124. delete copyBlocks['block-package'];
  125. const blocksArr = Object.entries(copyBlocks).map(([key, block]) => {
  126. const localeKey = `workflow.blocks.${key}.name`;
  127. return {
  128. ...block,
  129. id: key,
  130. name: te(localeKey) ? t(localeKey) : block.name,
  131. };
  132. });
  133. const descriptionCollapsed = ref(true);
  134. const query = ref('');
  135. const pinnedBlocks = ref([]);
  136. const blocks = computed(() =>
  137. blocksArr.reduce((arr, block) => {
  138. if (
  139. block.name.toLocaleLowerCase().includes(query.value.toLocaleLowerCase())
  140. ) {
  141. (arr[block.category] = arr[block.category] || []).push(block);
  142. }
  143. return arr;
  144. }, {})
  145. );
  146. const pinnedBlocksList = computed(() =>
  147. pinnedBlocks.value
  148. .map((id) => {
  149. const namePath = `workflow.blocks.${id}.name`;
  150. return {
  151. ...copyBlocks[id],
  152. id,
  153. name: te(namePath) ? t(namePath) : copyBlocks[id].name,
  154. };
  155. })
  156. .filter(({ name }) =>
  157. name.toLocaleLowerCase().includes(query.value.toLocaleLowerCase())
  158. )
  159. );
  160. function updateWorkflowIcon(value) {
  161. if (!value.startsWith('http')) return;
  162. const iconUrl = value.slice(0, 1024);
  163. emit('update', { icon: iconUrl });
  164. }
  165. function pinBlock({ id }) {
  166. const index = pinnedBlocks.value.indexOf(id);
  167. if (index !== -1) pinnedBlocks.value.splice(index, 1);
  168. else pinnedBlocks.value.push(id);
  169. }
  170. watch(
  171. pinnedBlocks,
  172. () => {
  173. browser.storage.local.set({
  174. pinnedBlocks: toRaw(pinnedBlocks.value),
  175. });
  176. },
  177. { deep: true }
  178. );
  179. onMounted(() => {
  180. browser.storage.local.get('pinnedBlocks').then((item) => {
  181. pinnedBlocks.value = item.pinnedBlocks || [];
  182. });
  183. });
  184. </script>