WorkflowShareTeam.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. <template>
  2. <ui-card class="share-workflow scroll w-full max-w-4xl overflow-auto">
  3. <template v-if="!isUpdate">
  4. <h1 class="text-xl font-semibold">Share workflow with team</h1>
  5. <p class="text-gray-600 dark:text-gray-200">
  6. This workflow will be shared with your team
  7. </p>
  8. </template>
  9. <p v-else class="font-semibold">Update workflow</p>
  10. <div class="mt-4 flex items-start">
  11. <div class="mr-8 flex-1">
  12. <div class="flex items-center">
  13. <ui-input
  14. v-model="state.workflow.name"
  15. :label="t('workflow.name')"
  16. type="text"
  17. name="workflow name"
  18. class="flex-1"
  19. />
  20. <ui-select
  21. v-if="!isUpdate"
  22. v-model="state.activeTeam"
  23. label="Select team"
  24. class="ml-4"
  25. style="max-width: 220px"
  26. >
  27. <option
  28. v-for="team in state.userTeams"
  29. :key="team.id"
  30. :value="team.id"
  31. >
  32. {{ team.name }}
  33. </option>
  34. </ui-select>
  35. </div>
  36. <div class="relative my-2">
  37. <label
  38. for="short-description"
  39. class="ml-2 text-sm text-gray-600 dark:text-gray-200"
  40. >
  41. Short description
  42. </label>
  43. <ui-textarea
  44. id="short-description"
  45. v-model="state.workflow.description"
  46. :max="300"
  47. label="Short description"
  48. placeholder="Write here..."
  49. class="scroll h-28 w-full resize-none"
  50. />
  51. <p
  52. class="absolute bottom-2 right-2 text-sm text-gray-600 dark:text-gray-200"
  53. >
  54. {{ state.workflow.description.length }}/300
  55. </p>
  56. </div>
  57. <shared-wysiwyg
  58. v-model="state.workflow.content"
  59. :placeholder="t('common.description')"
  60. :limit="5000"
  61. class="content-editor bg-box-transparent prose prose-zinc relative max-w-none rounded-lg p-4 dark:prose-invert"
  62. @count="state.contentLength = $event"
  63. >
  64. <template #append>
  65. <p
  66. class="absolute bottom-2 right-2 text-sm text-gray-600 dark:text-gray-200"
  67. >
  68. {{ state.contentLength }}/5000
  69. </p>
  70. </template>
  71. </shared-wysiwyg>
  72. </div>
  73. <div class="sticky top-4 w-64 pb-4">
  74. <template v-if="isUpdate">
  75. <ui-button
  76. variant="accent"
  77. class="w-full"
  78. @click="$emit('update', state.workflow)"
  79. >
  80. Save
  81. </ui-button>
  82. <ui-button class="mt-2 w-full" @click="$emit('close')">
  83. {{ t('common.cancel') }}
  84. </ui-button>
  85. </template>
  86. <template v-else>
  87. <div class="flex">
  88. <ui-button
  89. v-tooltip="'Save as draft'"
  90. :disabled="state.isPublishing"
  91. icon
  92. @click="saveDraft"
  93. >
  94. <v-remixicon name="riSaveLine" />
  95. </ui-button>
  96. <ui-button
  97. :loading="state.isPublishing"
  98. :disabled="!state.workflow.name.trim()"
  99. variant="accent"
  100. class="ml-2 w-full"
  101. @click="publishWorkflow"
  102. >
  103. Publish
  104. </ui-button>
  105. </div>
  106. <ui-button
  107. :disabled="state.isPublishing"
  108. class="mt-2 w-full"
  109. @click="$emit('close')"
  110. >
  111. Cancel
  112. </ui-button>
  113. </template>
  114. <ui-select
  115. v-model="state.workflow.category"
  116. class="mt-4 w-full"
  117. :label="t('common.category')"
  118. >
  119. <option value="">(none)</option>
  120. <option
  121. v-for="(category, id) in workflowCategories"
  122. :key="id"
  123. :value="id"
  124. >
  125. {{ category }}
  126. </option>
  127. </ui-select>
  128. <span class="ml-2 mt-5 block text-sm text-gray-600 dark:text-gray-200">
  129. Environment
  130. </span>
  131. <ui-tabs v-model="state.workflow.tag" type="fill" fill>
  132. <ui-tab value="stage"> Stage </ui-tab>
  133. <ui-tab value="production"> Production </ui-tab>
  134. </ui-tabs>
  135. </div>
  136. </div>
  137. </ui-card>
  138. </template>
  139. <script setup>
  140. import { reactive, watch, onMounted } from 'vue';
  141. import { useI18n } from 'vue-i18n';
  142. import { useToast } from 'vue-toastification';
  143. import browser from 'webextension-polyfill';
  144. import cloneDeep from 'lodash.clonedeep';
  145. import { fetchApi } from '@/utils/api';
  146. import { useUserStore } from '@/stores/user';
  147. import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
  148. import { workflowCategories } from '@/utils/shared';
  149. import { parseJSON, debounce } from '@/utils/helper';
  150. import { convertWorkflow } from '@/utils/workflowData';
  151. import { registerWorkflowTrigger } from '@/utils/workflowTrigger';
  152. import SharedWysiwyg from '@/components/newtab/shared/SharedWysiwyg.vue';
  153. const props = defineProps({
  154. workflow: {
  155. type: Object,
  156. default: () => ({}),
  157. },
  158. isUpdate: Boolean,
  159. });
  160. const emit = defineEmits(['close', 'publish', 'change', 'update']);
  161. const { t } = useI18n();
  162. const toast = useToast();
  163. const userStore = useUserStore();
  164. const teamWorkflowStore = useTeamWorkflowStore();
  165. const state = reactive({
  166. userTeams: [],
  167. activeTeam: null,
  168. contentLength: 0,
  169. isPublishing: false,
  170. workflow: JSON.parse(JSON.stringify(props.workflow)),
  171. });
  172. async function publishWorkflow() {
  173. try {
  174. state.isPublishing = true;
  175. const workflow = convertWorkflow(state.workflow, ['id', 'category']);
  176. workflow.name = workflow.name || 'unnamed';
  177. workflow.tag = state.workflow.tag || 'stage';
  178. workflow.content = state.workflow.content || null;
  179. workflow.description = state.workflow.description.slice(0, 300);
  180. workflow.drawflow = parseJSON(workflow.drawflow, workflow.drawflow);
  181. delete workflow.extVersion;
  182. const response = await fetchApi(`/teams/${state.activeTeam}/workflows`, {
  183. method: 'POST',
  184. body: JSON.stringify({ workflow }),
  185. });
  186. const result = await response.json();
  187. if (!response.ok) {
  188. const error = new Error(response.statusText);
  189. error.data = result.data;
  190. throw error;
  191. }
  192. workflow.id = result.id;
  193. workflow.teamId = result.teamId;
  194. workflow.createdAt = Date.now();
  195. workflow.drawflow = props.workflow.drawflow;
  196. await teamWorkflowStore.insert(state.activeTeam, cloneDeep(workflow));
  197. state.isPublishing = false;
  198. const triggerBlock = workflow.drawflow.nodes?.find(
  199. (node) => node.label === 'trigger'
  200. );
  201. if (triggerBlock) {
  202. await registerWorkflowTrigger(workflow.id, triggerBlock);
  203. }
  204. toast('Successfully share the workflow with your team');
  205. emit('publish');
  206. } catch (error) {
  207. let errorMessage = t('message.somethingWrong');
  208. if (error.data && error.data.show) {
  209. errorMessage = error.message;
  210. }
  211. toast.error(errorMessage);
  212. console.error(error);
  213. state.isPublishing = false;
  214. }
  215. }
  216. function saveDraft() {
  217. const key = `draft-team:${props.workflow.id}`;
  218. browser.storage.local.set({
  219. [key]: {
  220. name: state.workflow.name,
  221. tag: state.tag,
  222. content: state.workflow.content,
  223. category: state.workflow.category,
  224. description: state.workflow.description,
  225. },
  226. });
  227. }
  228. watch(
  229. () => state.workflow,
  230. debounce(() => {
  231. emit('change', state.workflow);
  232. }, 200),
  233. { deep: true }
  234. );
  235. onMounted(() => {
  236. if (!props.isUpdate) {
  237. const key = `draft-team:${props.workflow.id}`;
  238. browser.storage.local.get(key).then((data) => {
  239. Object.assign(state.workflow, data[key]);
  240. if (!state.workflow.tag) {
  241. state.workflow.tag = 'stage';
  242. }
  243. });
  244. state.userTeams = userStore.user.teams.filter((team) =>
  245. team.access.some((item) => ['owner', 'create'].includes(item))
  246. );
  247. if (state.userTeams[0]) state.activeTeam = state.userTeams[0].id;
  248. }
  249. });
  250. </script>
  251. <style scoped>
  252. .share-workflow {
  253. min-height: 500px;
  254. max-height: calc(100vh - 4rem);
  255. }
  256. .editor-menu-btn {
  257. @apply p-1 rounded-md transition;
  258. }
  259. </style>
  260. <style>
  261. .content-editor .ProseMirror {
  262. min-height: 200px;
  263. }
  264. .content-editor .ProseMirror :first-child {
  265. margin-top: 0 !important;
  266. }
  267. </style>