sanity-plugin-workflow 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Simeon Griggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ > This is a **Sanity Studio v3** plugin.
2
+
3
+ # sanity-plugin-workflow
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ npm install --save sanity-plugin-workflow
9
+ ```
10
+
11
+ or
12
+
13
+ ```
14
+ yarn add sanity-plugin-workflow
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ Add it as a plugin in sanity.config.ts (or .js):
20
+
21
+ ```js
22
+ import {createConfig} from 'sanity'
23
+ import {workflow} from 'sanity-plugin-workflow'
24
+
25
+ export const createConfig({
26
+ // all other settings ...
27
+ plugins: [
28
+ workflow({
29
+ // Required
30
+ schemaTypes: [],
31
+ // Optional
32
+ states: [],
33
+ })
34
+ ]
35
+ })
36
+ ```
37
+
38
+ ## License
39
+
40
+ MIT © Simeon Griggs
41
+ See LICENSE
42
+
43
+
44
+ ## License
45
+
46
+ [MIT](LICENSE) © Simeon Griggs
47
+
48
+
49
+ ## Develop & test
50
+
51
+ This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit)
52
+ with default configuration for build & watch scripts.
53
+
54
+ See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio)
55
+ on how to run this plugin with hotreload in the studio.
56
+
57
+ ### Release new version
58
+
59
+ Run ["CI & Release" workflow](https://github.com/sanity-io/sanity-plugin-workflow/actions/workflows/main.yml).
60
+ Make sure to select the main branch and check "Release new version".
61
+
62
+ Semantic release will only release on configured branches, so it is safe to run release on any branch.
@@ -0,0 +1 @@
1
+ function t(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);e&&(o=o.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,o)}return n}function e(e){for(var o=1;o<arguments.length;o++){var i=null!=arguments[o]?arguments[o]:{};o%2?t(Object(i),!0).forEach((function(t){n(e,t,i[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(i)):t(Object(i)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(i,t))}))}return e}function n(t,e,n){return(e=function(t){var e=function(t,e){if("object"!=typeof t||null===t)return t;var n=t[Symbol.toPrimitive];if(void 0!==n){var o=n.call(t,e||"default");if("object"!=typeof o)return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===e?String:Number)(t)}(t,"string");return"symbol"==typeof e?e:String(e)}(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}import{UserAvatar as o,useClient as i,useSchema as r,Preview as a,useDocumentOperation as s,defineType as l,defineField as d,defineArrayMember as c,definePlugin as u}from"sanity";import{EditIcon as p,AddIcon as m,DragHandleIcon as h,ArrowRightIcon as f,ArrowLeftIcon as g,SplitVerticalIcon as v,CheckmarkIcon as y}from"@sanity/icons";import{jsx as b,jsxs as w,Fragment as _}from"react/jsx-runtime";import k from"react";import{Button as I,Flex as D,Box as C,Text as O,useToast as j,Popover as P,useTheme as x,Card as S,Stack as E,Container as T,Grid as L,Label as R,Spinner as M}from"@sanity/ui";import{UserSelectMenu as W,useListeningQuery as A,useProjectUsers as F,Feedback as H}from"sanity-plugin-utils";import{DragDropContext as q,Droppable as z,Draggable as N}from"react-beautiful-dnd";import{useRouter as B}from"sanity/router";function Y(t){const{id:e,type:n}=t,{navigateIntent:o}=B();return b(I,{onClick:()=>o("edit",{id:e,type:n}),mode:"ghost",fontSize:1,padding:2,tabIndex:-1,icon:p,text:"Edit"})}function $(t){const{users:e,max:n=3}=t,i=null==e?void 0:e.length,r=k.useMemo((()=>e.slice(0,n)),[e]);return(null==e?void 0:e.length)?w(D,{align:"center",children:[r.map((t=>b(C,{style:{marginRight:-5},children:b(o,{user:t})},t.id))),i>n&&b(C,{paddingLeft:2,children:w(O,{size:1,children:["+",i-n]})})]}):null}function U(t){const{assignees:e,userList:n,documentId:o}=t,r=i(),a=j(),[s,l]=k.useState(""),d=k.useCallback((t=>{if(!t)return a.push({status:"error",title:"No user selected"});r.patch("workflow-metadata.".concat(o)).setIfMissing({assignees:[]}).insert("after","assignees[-1]",[t]).commit().then((()=>a.push({title:"Assigned user to document",description:t,status:"success"}))).catch((e=>(console.error(e),a.push({title:"Failed to add assignee",description:t,status:"error"}))))}),[o,r,a]),c=k.useCallback(((t,e)=>{r.patch("workflow-metadata.".concat(t)).unset(['assignees[@ == "'.concat(e,'"]')]).commit().then((t=>t)).catch((e=>(console.error(e),a.push({title:"Failed to remove assignee",description:t,status:"error"}))))}),[r,a]),u=k.useCallback((t=>{r.patch("workflow-metadata.".concat(t)).unset(["assignees"]).commit().then((t=>t)).catch((e=>(console.error(e),a.push({title:"Failed to clear assignees",description:t,status:"error"}))))}),[r,a]);return b(P,{content:b(W,{style:{maxHeight:300},value:e||[],userList:n,onAdd:d,onClear:u,onRemove:c,open:s===o}),portal:!0,open:s===o,children:e&&0!==e.length?b(I,{onClick:()=>l(o),padding:0,mode:"bleed",style:{width:"100%"},children:b($,{users:n.filter((t=>e.includes(t.id)))})}):b(I,{onClick:()=>l(o),fontSize:1,padding:2,tabIndex:-1,icon:m,text:"Assign",tone:"positive"})})}function V(t){var e;const{userList:n,isDragging:o,item:i}=t,{assignees:s=[],documentId:l}=null!=(e=i._metadata)?e:{},d=r(),c=x().sanity.color.dark;return b(C,{paddingY:2,paddingX:3,children:b(S,{radius:2,shadow:o?3:1,tone:o?"positive":c?"transparent":"default",children:w(E,{children:[b(S,{borderBottom:!0,radius:2,padding:3,paddingLeft:2,tone:"inherit",style:{pointerEvents:"none"},children:w(D,{align:"center",justify:"space-between",gap:1,children:[b(a,{layout:"default",value:i,schemaType:d.get(i._type)}),b(h,{style:{flexShrink:0}})]})}),b(S,{padding:2,radius:2,tone:"inherit",children:w(D,{align:"center",justify:"space-between",gap:1,children:[l&&b(U,{userList:n,assignees:s,documentId:l}),b(Y,{id:i._id,type:i._type})]})})]})})})}function X(t){const{_id:e,_type:n,documentId:o,state:i,onComplete:r}=t,a=s(o,n),l=e.startsWith("drafts."),d=j();return l&&"publish"===i.operation?a.publish.disabled||(a.publish.execute(),r(e),d.push({title:"Published Document",description:o,status:"success"})):l||"unpublish"!==i.operation?r(e):a.unpublish.disabled||(a.unpublish.execute(),r(e),d.push({title:"Unpublished Document",description:o,status:"success"})),w(S,{padding:3,shadow:2,tone:"primary",children:["Mutating: ",e," to ",i.title]})}const G='{\n "documents": '.concat("*[_type in $schemaTypes]{ _id, _type, _rev }",',\n "metadata": ').concat('*[_type == "workflow.metadata"]{\n _rev,\n assignees,\n documentId,\n state\n}',"\n}"),J={documents:[],metadata:[]};function K(t){var n,o;const{schemaTypes:r=[],states:a=[]}=null!=(o=null==(n=null==t?void 0:t.tool)?void 0:n.options)?o:{},[s,l]=k.useState([]),d=k.useCallback((t=>{l((e=>e.filter((e=>e._id!==t))))}),[]),c=i(),u=j(),p=x().sanity.color.dark?"default":"transparent",m=F()||[],{workflowData:h,operations:f}=function(t){const n=j(),o=i(),[r,a]=k.useState([]),{data:s,loading:l,error:d}=A(G,{params:{schemaTypes:t},initialValue:J});k.useEffect((()=>{if(s){const t=s.documents.reduce(((t,n)=>{const o=s.metadata.find((t=>t.documentId===n._id.replace("drafts.","")));if(!o)return[...t,e({_metadata:null},n)];const i=e({_metadata:o},n);return n._id.startsWith("drafts.")?[...t,i]:Boolean(s.documents.find((t=>t._id==="drafts.".concat(n._id))))?t:[...t,i]}),[]);a(t)}}),[s]);const c=k.useCallback(((t,i,s)=>{const l=r,d=r.map((n=>{var o;return(null==(o=null==n?void 0:n._metadata)?void 0:o.documentId)===t?e(e({},n),{},{_metadata:e(e({},n._metadata),{},{state:i.droppableId})}):n}));a(d);const c=i.droppableId,u=s.find((t=>t.id===c)),p=r.find((e=>{var n;return(null==(n=null==e?void 0:e._metadata)?void 0:n.documentId)===t}));if(!(null==u?void 0:u.id))return n.push({title:"Could not find target state ".concat(c),status:"error"}),null;if(!p)return n.push({title:"Could not find dragged document in data",status:"error"}),null;const{_id:m,_type:h}=p,{_rev:f,documentId:g}=p._metadata||{};return o.patch("workflow-metadata.".concat(g)).ifRevisionId(f).set({state:c}).commit().then((()=>{var t;return n.push({title:'Moved to "'.concat(null!=(t=null==u?void 0:u.title)?t:c,'"'),description:g,status:"success"})})).catch((()=>{var t;return a(l),n.push({title:'Failed to move to "'.concat(null!=(t=null==u?void 0:u.title)?t:c,'"'),description:g,status:"error"})})),{_id:m,_type:h,documentId:g,state:u}}),[o,n,r]);return{workflowData:{data:r,loading:l,error:d},operations:{move:c}}}(r),{data:g,loading:v,error:y}=h,{move:O}=f,P=g.filter((t=>!t._metadata)).map((t=>t._id.replace("drafts.",""))),E=k.useCallback((async t=>{u.push({title:"Importing documents",status:"info"});const e=t.reduce(((t,e)=>t.createOrReplace({_id:"workflow-metadata.".concat(e),_type:"workflow.metadata",state:a[0].id,documentId:e})),c.transaction());await e.commit(),u.push({title:"Imported documents",status:"success"})}),[]),W=k.useCallback((t=>{const{draggableId:e,source:n,destination:o}=t;if(console.log("sending ".concat(e," from ").concat(n.droppableId," to ").concat(null==o?void 0:o.droppableId)),!o||o.droppableId===n.droppableId)return;const i=O(e,o,a);i&&l((t=>[...t,i]))}),[O,a]);return(null==a?void 0:a.length)?y?b(T,{width:1,padding:5,children:b(H,{tone:"critical",title:"Error with query"})}):w(_,{children:[s.length?b("div",{style:{position:"absolute",bottom:0,background:"red"},children:s.map((t=>b(X,e(e({},t),{},{onComplete:d}),t._id)))}):null,P.length>0&&b(C,{padding:5,children:b(S,{border:!0,padding:3,tone:"caution",children:b(D,{align:"center",justify:"center",children:w(I,{onClick:()=>E(P),children:["Import ",P.length," Missing"," ",1===P.length?"Document":"Documents"," ","into Workflow"]})})})}),b(q,{onDragEnd:W,children:b(L,{columns:a.length,height:"fill",children:a.map(((t,n)=>w(S,{borderLeft:n>0,children:[b(S,{paddingY:4,padding:3,style:{pointerEvents:"none"},children:b(R,{children:t.title})}),b(z,{droppableId:t.id,children:(n,o)=>{return w(S,{ref:n.innerRef,tone:o.isDraggingOver?"primary":p,height:"fill",children:[v?b(D,{padding:5,align:"center",justify:"center",children:b(M,{muted:!0})}):null,g.length>0&&(i=g,r=t.id,i.filter((t=>{var e;return(null==(e=null==t?void 0:t._metadata)?void 0:e.state)===r}))).map(((t,n)=>{var o,i;return b(N,{draggableId:null==(i=null==t?void 0:t._metadata)?void 0:i.documentId,index:n,children:(n,o)=>b("div",e(e(e({ref:n.innerRef},n.draggableProps),n.dragHandleProps),{},{children:b(V,{isDragging:o.isDragging,item:t,userList:m})}))},null==(o=null==t?void 0:t._metadata)?void 0:o.documentId)}))]});var i,r}})]},t.id)))})})]}):b(T,{width:1,padding:5,children:b(H,{tone:"caution",title:"Plugin options error",description:"No States defined in plugin config"})})}var Q=t=>l({type:"document",name:"workflow.metadata",title:"Workflow metadata",liveEdit:!0,fields:[d({name:"state",type:"string",options:{list:t.map((t=>({value:t.id,title:t.title})))}}),d({name:"documentId",title:"Document ID",type:"string",readOnly:!0}),d({type:"array",name:"assignees",description:"The people who are assigned to move this further in the workflow.",of:[c({type:"string"})]})]});function Z(t,e){const{data:n,loading:o,error:i}=A('*[_type == "workflow.metadata" && documentId == $id][0]',{params:{id:t}});return(null==n?void 0:n.state)?{data:{metadata:n,state:e.find((t=>t.id===n.state))},loading:o,error:i}:{data:{},loading:o,error:i}}function tt(t,e){const{id:n}=t,{data:o,loading:i,error:r}=Z(n,e),{state:a}=o;return i||r?(r&&console.error(r),null):a?{label:a.title,title:a.title,color:null==a?void 0:a.color}:null}function et(t,e){const{id:n}=t,{data:o,loading:r,error:a}=Z(n,e),{state:s}=o,l=i(),d=j();if(r||a)return a&&console.error(a),null;if(!s)return null;const c=e.findIndex((t=>t.id===s.id)),u=e[c+1];return u?{icon:f,label:"Promote",title:'Promote State to "'.concat(u.title,'"'),onHandle:()=>{return e=n,o=u,void l.patch("workflow-metadata.".concat(e)).set({state:o.id}).commit().then((()=>{t.onComplete(),d.push({status:"success",title:"Document promoted to ".concat(o.title)})})).catch((e=>{t.onComplete(),console.error(e),d.push({status:"error",title:"Document promotion failed"})}));var e,o}}:null}function nt(t,e){const{id:n}=t,{data:o,loading:r,error:a}=Z(n,e),{state:s}=o,l=i(),d=j();if(r||a)return a&&console.error(a),null;if(!s)return null;const c=e.findIndex((t=>t.id===s.id)),u=e[c-1];return u?{icon:g,label:"Demote",title:'Demote State to "'.concat(u.title,'"'),onHandle:()=>{return e=n,o=u,void l.patch("workflow-metadata.".concat(e)).set({state:o.id}).commit().then((()=>{t.onComplete(),d.push({status:"success",title:"Document demoted to ".concat(o.title)})})).catch((e=>{t.onComplete(),console.error(e),d.push({status:"error",title:"Document demotion failed"})}));var e,o}}:null}const ot={schemaTypes:[],states:[{id:"draft",title:"Draft",operation:"unpublish"},{id:"inReview",title:"In review",operation:null,color:"primary"},{id:"approved",title:"Approved",operation:null,color:"success",icon:y},{id:"changesRequested",title:"Changes requested",operation:null,color:"warning"},{id:"published",title:"Published",operation:"publish",color:"success"}]},it=u((function(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:ot;const{schemaTypes:n,states:o}=e(e({},ot),t);if(!(null==o?void 0:o.length))throw new Error("Workflow: Missing states in config");return{name:"sanity-plugin-workflow",schema:{types:[Q(o)]},document:{actions:(t,e)=>n.includes(e.schemaType)?[t=>et(t,o),t=>nt(t,o),...t]:t,badges:(t,e)=>n.includes(e.schemaType)?[t=>tt(t,o),...t]:t},tools:[{name:"workflow",title:"Workflow",component:K,icon:v,options:{schemaTypes:n,states:o}}]}}));export{it as workflow};//# sourceMappingURL=index.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/lib/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function t(t){for(var r=1;r<arguments.length;r++){var o=null!=arguments[r]?arguments[r]:{};r%2?e(Object(o),!0).forEach((function(e){n(t,e,o[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(o)):e(Object(o)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(o,e))}))}return t}function n(e,t,n){return(t=function(e){var t=function(e,t){if("object"!=typeof e||null===e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t||"default");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:String(t)}(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}Object.defineProperty(exports,"__esModule",{value:!0});var r=require("sanity"),o=require("@sanity/icons"),i=require("react/jsx-runtime"),a=require("react"),s=require("@sanity/ui"),l=require("sanity-plugin-utils"),d=require("react-beautiful-dnd"),c=require("sanity/router");function u(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var p=u(a);function m(e){const{id:t,type:n}=e,{navigateIntent:r}=c.useRouter();return i.jsx(s.Button,{onClick:()=>r("edit",{id:t,type:n}),mode:"ghost",fontSize:1,padding:2,tabIndex:-1,icon:o.EditIcon,text:"Edit"})}function f(e){const{users:t,max:n=3}=e,o=null==t?void 0:t.length,a=p.default.useMemo((()=>t.slice(0,n)),[t]);return(null==t?void 0:t.length)?i.jsxs(s.Flex,{align:"center",children:[a.map((e=>i.jsx(s.Box,{style:{marginRight:-5},children:i.jsx(r.UserAvatar,{user:e})},e.id))),o>n&&i.jsx(s.Box,{paddingLeft:2,children:i.jsxs(s.Text,{size:1,children:["+",o-n]})})]}):null}function h(e){const{assignees:t,userList:n,documentId:a}=e,d=r.useClient(),c=s.useToast(),[u,m]=p.default.useState(""),h=p.default.useCallback((e=>{if(!e)return c.push({status:"error",title:"No user selected"});d.patch("workflow-metadata.".concat(a)).setIfMissing({assignees:[]}).insert("after","assignees[-1]",[e]).commit().then((()=>c.push({title:"Assigned user to document",description:e,status:"success"}))).catch((t=>(console.error(t),c.push({title:"Failed to add assignee",description:e,status:"error"}))))}),[a,d,c]),g=p.default.useCallback(((e,t)=>{d.patch("workflow-metadata.".concat(e)).unset(['assignees[@ == "'.concat(t,'"]')]).commit().then((e=>e)).catch((t=>(console.error(t),c.push({title:"Failed to remove assignee",description:e,status:"error"}))))}),[d,c]),v=p.default.useCallback((e=>{d.patch("workflow-metadata.".concat(e)).unset(["assignees"]).commit().then((e=>e)).catch((t=>(console.error(t),c.push({title:"Failed to clear assignees",description:e,status:"error"}))))}),[d,c]);return i.jsx(s.Popover,{content:i.jsx(l.UserSelectMenu,{style:{maxHeight:300},value:t||[],userList:n,onAdd:h,onClear:v,onRemove:g,open:u===a}),portal:!0,open:u===a,children:t&&0!==t.length?i.jsx(s.Button,{onClick:()=>m(a),padding:0,mode:"bleed",style:{width:"100%"},children:i.jsx(f,{users:n.filter((e=>t.includes(e.id)))})}):i.jsx(s.Button,{onClick:()=>m(a),fontSize:1,padding:2,tabIndex:-1,icon:o.AddIcon,text:"Assign",tone:"positive"})})}function g(e){var t;const{userList:n,isDragging:a,item:l}=e,{assignees:d=[],documentId:c}=null!=(t=l._metadata)?t:{},u=r.useSchema(),p=s.useTheme().sanity.color.dark?"transparent":"default";return i.jsx(s.Box,{paddingY:2,paddingX:3,children:i.jsx(s.Card,{radius:2,shadow:a?3:1,tone:a?"positive":p,children:i.jsxs(s.Stack,{children:[i.jsx(s.Card,{borderBottom:!0,radius:2,padding:3,paddingLeft:2,tone:"inherit",style:{pointerEvents:"none"},children:i.jsxs(s.Flex,{align:"center",justify:"space-between",gap:1,children:[i.jsx(r.Preview,{layout:"default",value:l,schemaType:u.get(l._type)}),i.jsx(o.DragHandleIcon,{style:{flexShrink:0}})]})}),i.jsx(s.Card,{padding:2,radius:2,tone:"inherit",children:i.jsxs(s.Flex,{align:"center",justify:"space-between",gap:1,children:[c&&i.jsx(h,{userList:n,assignees:d,documentId:c}),i.jsx(m,{id:l._id,type:l._type})]})})]})})})}function v(e){const{_id:t,_type:n,documentId:o,state:a,onComplete:l}=e,d=r.useDocumentOperation(o,n),c=t.startsWith("drafts."),u=s.useToast();return c&&"publish"===a.operation?d.publish.disabled||(d.publish.execute(),l(t),u.push({title:"Published Document",description:o,status:"success"})):c||"unpublish"!==a.operation?l(t):d.unpublish.disabled||(d.unpublish.execute(),l(t),u.push({title:"Unpublished Document",description:o,status:"success"})),i.jsxs(s.Card,{padding:3,shadow:2,tone:"primary",children:["Mutating: ",t," to ",a.title]})}const y='{\n "documents": '.concat("*[_type in $schemaTypes]{ _id, _type, _rev }",',\n "metadata": ').concat('*[_type == "workflow.metadata"]{\n _rev,\n assignees,\n documentId,\n state\n}',"\n}"),b={documents:[],metadata:[]};function x(e){var n,o;const{schemaTypes:a=[],states:c=[]}=null!=(o=null==(n=null==e?void 0:e.tool)?void 0:n.options)?o:{},[u,m]=p.default.useState([]),f=p.default.useCallback((e=>{m((t=>t.filter((t=>t._id!==e))))}),[]),h=r.useClient(),x=s.useToast(),j=s.useTheme().sanity.color.dark?"default":"transparent",w=l.useProjectUsers()||[],{workflowData:I,operations:k}=function(e){const n=s.useToast(),o=r.useClient(),[i,a]=p.default.useState([]),{data:d,loading:c,error:u}=l.useListeningQuery(y,{params:{schemaTypes:e},initialValue:b});p.default.useEffect((()=>{if(d){const e=d.documents.reduce(((e,n)=>{const r=d.metadata.find((e=>e.documentId===n._id.replace("drafts.","")));if(!r)return[...e,t({_metadata:null},n)];const o=t({_metadata:r},n);return n._id.startsWith("drafts.")?[...e,o]:Boolean(d.documents.find((e=>e._id==="drafts.".concat(n._id))))?e:[...e,o]}),[]);a(e)}}),[d]);const m=p.default.useCallback(((e,r,s)=>{const l=i,d=i.map((n=>{var o;return(null==(o=null==n?void 0:n._metadata)?void 0:o.documentId)===e?t(t({},n),{},{_metadata:t(t({},n._metadata),{},{state:r.droppableId})}):n}));a(d);const c=r.droppableId,u=s.find((e=>e.id===c)),p=i.find((t=>{var n;return(null==(n=null==t?void 0:t._metadata)?void 0:n.documentId)===e}));if(!(null==u?void 0:u.id))return n.push({title:"Could not find target state ".concat(c),status:"error"}),null;if(!p)return n.push({title:"Could not find dragged document in data",status:"error"}),null;const{_id:m,_type:f}=p,{_rev:h,documentId:g}=p._metadata||{};return o.patch("workflow-metadata.".concat(g)).ifRevisionId(h).set({state:c}).commit().then((()=>{var e;return n.push({title:'Moved to "'.concat(null!=(e=null==u?void 0:u.title)?e:c,'"'),description:g,status:"success"})})).catch((()=>{var e;return a(l),n.push({title:'Failed to move to "'.concat(null!=(e=null==u?void 0:u.title)?e:c,'"'),description:g,status:"error"})})),{_id:m,_type:f,documentId:g,state:u}}),[o,n,i]);return{workflowData:{data:i,loading:c,error:u},operations:{move:m}}}(a),{data:_,loading:C,error:D}=I,{move:O}=k,P=_.filter((e=>!e._metadata)).map((e=>e._id.replace("drafts.",""))),T=p.default.useCallback((async e=>{x.push({title:"Importing documents",status:"info"});const t=e.reduce(((e,t)=>e.createOrReplace({_id:"workflow-metadata.".concat(t),_type:"workflow.metadata",state:c[0].id,documentId:t})),h.transaction());await t.commit(),x.push({title:"Imported documents",status:"success"})}),[]),S=p.default.useCallback((e=>{const{draggableId:t,source:n,destination:r}=e;if(console.log("sending ".concat(t," from ").concat(n.droppableId," to ").concat(null==r?void 0:r.droppableId)),!r||r.droppableId===n.droppableId)return;const o=O(t,r,c);o&&m((e=>[...e,o]))}),[O,c]);return(null==c?void 0:c.length)?D?i.jsx(s.Container,{width:1,padding:5,children:i.jsx(l.Feedback,{tone:"critical",title:"Error with query"})}):i.jsxs(i.Fragment,{children:[u.length?i.jsx("div",{style:{position:"absolute",bottom:0,background:"red"},children:u.map((e=>i.jsx(v,t(t({},e),{},{onComplete:f}),e._id)))}):null,P.length>0&&i.jsx(s.Box,{padding:5,children:i.jsx(s.Card,{border:!0,padding:3,tone:"caution",children:i.jsx(s.Flex,{align:"center",justify:"center",children:i.jsxs(s.Button,{onClick:()=>T(P),children:["Import ",P.length," Missing"," ",1===P.length?"Document":"Documents"," ","into Workflow"]})})})}),i.jsx(d.DragDropContext,{onDragEnd:S,children:i.jsx(s.Grid,{columns:c.length,height:"fill",children:c.map(((e,n)=>i.jsxs(s.Card,{borderLeft:n>0,children:[i.jsx(s.Card,{paddingY:4,padding:3,style:{pointerEvents:"none"},children:i.jsx(s.Label,{children:e.title})}),i.jsx(d.Droppable,{droppableId:e.id,children:(n,r)=>{return i.jsxs(s.Card,{ref:n.innerRef,tone:r.isDraggingOver?"primary":j,height:"fill",children:[C?i.jsx(s.Flex,{padding:5,align:"center",justify:"center",children:i.jsx(s.Spinner,{muted:!0})}):null,_.length>0&&(o=_,a=e.id,o.filter((e=>{var t;return(null==(t=null==e?void 0:e._metadata)?void 0:t.state)===a}))).map(((e,n)=>{var r,o;return i.jsx(d.Draggable,{draggableId:null==(o=null==e?void 0:e._metadata)?void 0:o.documentId,index:n,children:(n,r)=>i.jsx("div",t(t(t({ref:n.innerRef},n.draggableProps),n.dragHandleProps),{},{children:i.jsx(g,{isDragging:r.isDragging,item:e,userList:w})}))},null==(r=null==e?void 0:e._metadata)?void 0:r.documentId)}))]});var o,a}})]},e.id)))})})]}):i.jsx(s.Container,{width:1,padding:5,children:i.jsx(l.Feedback,{tone:"caution",title:"Plugin options error",description:"No States defined in plugin config"})})}var j=e=>r.defineType({type:"document",name:"workflow.metadata",title:"Workflow metadata",liveEdit:!0,fields:[r.defineField({name:"state",type:"string",options:{list:e.map((e=>({value:e.id,title:e.title})))}}),r.defineField({name:"documentId",title:"Document ID",type:"string",readOnly:!0}),r.defineField({type:"array",name:"assignees",description:"The people who are assigned to move this further in the workflow.",of:[r.defineArrayMember({type:"string"})]})]});function w(e,t){const{data:n,loading:r,error:o}=l.useListeningQuery('*[_type == "workflow.metadata" && documentId == $id][0]',{params:{id:e}});return(null==n?void 0:n.state)?{data:{metadata:n,state:t.find((e=>e.id===n.state))},loading:r,error:o}:{data:{},loading:r,error:o}}function I(e,t){const{id:n}=e,{data:r,loading:o,error:i}=w(n,t),{state:a}=r;return o||i?(i&&console.error(i),null):a?{label:a.title,title:a.title,color:null==a?void 0:a.color}:null}function k(e,t){const{id:n}=e,{data:i,loading:a,error:l}=w(n,t),{state:d}=i,c=r.useClient(),u=s.useToast();if(a||l)return l&&console.error(l),null;if(!d)return null;const p=t.findIndex((e=>e.id===d.id)),m=t[p+1];return m?{icon:o.ArrowRightIcon,label:"Promote",title:'Promote State to "'.concat(m.title,'"'),onHandle:()=>{return t=n,r=m,void c.patch("workflow-metadata.".concat(t)).set({state:r.id}).commit().then((()=>{e.onComplete(),u.push({status:"success",title:"Document promoted to ".concat(r.title)})})).catch((t=>{e.onComplete(),console.error(t),u.push({status:"error",title:"Document promotion failed"})}));var t,r}}:null}function _(e,t){const{id:n}=e,{data:i,loading:a,error:l}=w(n,t),{state:d}=i,c=r.useClient(),u=s.useToast();if(a||l)return l&&console.error(l),null;if(!d)return null;const p=t.findIndex((e=>e.id===d.id)),m=t[p-1];return m?{icon:o.ArrowLeftIcon,label:"Demote",title:'Demote State to "'.concat(m.title,'"'),onHandle:()=>{return t=n,r=m,void c.patch("workflow-metadata.".concat(t)).set({state:r.id}).commit().then((()=>{e.onComplete(),u.push({status:"success",title:"Document demoted to ".concat(r.title)})})).catch((t=>{e.onComplete(),console.error(t),u.push({status:"error",title:"Document demotion failed"})}));var t,r}}:null}const C={schemaTypes:[],states:[{id:"draft",title:"Draft",operation:"unpublish"},{id:"inReview",title:"In review",operation:null,color:"primary"},{id:"approved",title:"Approved",operation:null,color:"success",icon:o.CheckmarkIcon},{id:"changesRequested",title:"Changes requested",operation:null,color:"warning"},{id:"published",title:"Published",operation:"publish",color:"success"}]},D=r.definePlugin((function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:C;const{schemaTypes:n,states:r}=t(t({},C),e);if(!(null==r?void 0:r.length))throw new Error("Workflow: Missing states in config");return{name:"sanity-plugin-workflow",schema:{types:[j(r)]},document:{actions:(e,t)=>n.includes(t.schemaType)?[e=>k(e,r),e=>_(e,r),...e]:e,badges:(e,t)=>n.includes(t.schemaType)?[e=>I(e,r),...e]:e},tools:[{name:"workflow",title:"Workflow",component:x,icon:o.SplitVerticalIcon,options:{schemaTypes:n,states:r}}]}}));exports.workflow=D;//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,19 @@
1
+ import {Plugin as Plugin_2} from 'sanity'
2
+ import {default as React_2} from 'react'
3
+
4
+ declare type State = {
5
+ id: string
6
+ title: string
7
+ operation?: 'publish' | 'unpublish' | null
8
+ color?: 'primary' | 'success' | 'warning' | 'danger'
9
+ icon?: React_2.ReactNode | React_2.ComponentType
10
+ }
11
+
12
+ export declare const workflow: Plugin_2<WorkflowConfig>
13
+
14
+ declare type WorkflowConfig = {
15
+ schemaTypes: string[]
16
+ states?: State[]
17
+ }
18
+
19
+ export {}
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "sanity-plugin-workflow",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "A demonstration of a custom content publishing workflow using Sanity.",
5
+ "keywords": [
6
+ "sanity",
7
+ "sanity-plugin"
8
+ ],
9
+ "homepage": "https://github.com/sanity-io/sanity-plugin-workflow#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/sanity-io/sanity-plugin-workflow/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git@github.com:sanity-io/sanity-plugin-workflow.git"
16
+ },
17
+ "license": "MIT",
18
+ "author": "Simeon Griggs <simeon@sanity.io>",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./lib/src/index.d.ts",
22
+ "source": "./src/index.ts",
23
+ "import": "./lib/index.esm.js",
24
+ "require": "./lib/index.js",
25
+ "default": "./lib/index.esm.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "main": "./lib/index.js",
30
+ "module": "./lib/index.esm.js",
31
+ "source": "./src/index.ts",
32
+ "types": "./lib/src/index.d.ts",
33
+ "files": [
34
+ "src",
35
+ "lib",
36
+ "v2-incompatible.js",
37
+ "sanity.json"
38
+ ],
39
+ "scripts": {
40
+ "prebuild": "npm run clean && plugin-kit verify-package --silent && pkg-utils",
41
+ "build": "pkg-utils build",
42
+ "clean": "rimraf lib",
43
+ "format": "npx prettier --write . --ignore-path .gitignore",
44
+ "link-watch": "plugin-kit link-watch",
45
+ "lint": "eslint .",
46
+ "prepublishOnly": "npm run build",
47
+ "watch": "pkg-utils watch",
48
+ "prepare": "husky install"
49
+ },
50
+ "dependencies": {
51
+ "@sanity/icons": "^1.3.4",
52
+ "@sanity/incompatible-plugin": "^1.0.4",
53
+ "react-beautiful-dnd": "^13.1.1",
54
+ "react-fast-compare": "^3.2.0",
55
+ "sanity-plugin-utils": "^0.0.3"
56
+ },
57
+ "devDependencies": {
58
+ "@commitlint/cli": "^17.3.0",
59
+ "@commitlint/config-conventional": "^17.3.0",
60
+ "@sanity/pkg-utils": "^1.20.3",
61
+ "@sanity/plugin-kit": "^2.2.0",
62
+ "@sanity/semantic-release-preset": "^2.0.3",
63
+ "@types/react-beautiful-dnd": "^13.1.2",
64
+ "@typescript-eslint/eslint-plugin": "^5.46.1",
65
+ "@typescript-eslint/parser": "^5.46.1",
66
+ "eslint": "^8.29.0",
67
+ "eslint-config-prettier": "^8.5.0",
68
+ "eslint-config-sanity": "^6.0.0",
69
+ "eslint-plugin-prettier": "^4.2.1",
70
+ "eslint-plugin-react": "^7.31.11",
71
+ "eslint-plugin-react-hooks": "^4.6.0",
72
+ "husky": "^8.0.2",
73
+ "lint-staged": "^13.1.0",
74
+ "prettier": "^2.8.1",
75
+ "prettier-plugin-packagejson": "^2.3.0",
76
+ "react": "^18",
77
+ "rimraf": "^3.0.2",
78
+ "sanity": "^3.0.0",
79
+ "typescript": "^4.9.4"
80
+ },
81
+ "peerDependencies": {
82
+ "@sanity/ui": "^1.0.0",
83
+ "react": "^18",
84
+ "sanity": "^3.0.0"
85
+ },
86
+ "engines": {
87
+ "node": ">=14"
88
+ },
89
+ "sanityPlugin": {
90
+ "verifyPackage": {
91
+ "eslintImports": false
92
+ }
93
+ },
94
+ "yalcSig": "d290d75b60ad731b47370b9b1e6dba34"
95
+ }
package/sanity.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "parts": [
3
+ {
4
+ "implements": "part:@sanity/base/sanity-root",
5
+ "path": "./v2-incompatible.js"
6
+ }
7
+ ]
8
+ }
@@ -0,0 +1,62 @@
1
+ import {ArrowLeftIcon} from '@sanity/icons'
2
+ import {useToast} from '@sanity/ui'
3
+ import {DocumentActionProps, useClient} from 'sanity'
4
+ import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
5
+
6
+ import {State} from '../types'
7
+
8
+ export function DemoteAction(props: DocumentActionProps, states: State[]) {
9
+ const {id} = props
10
+ const {data, loading, error} = useWorkflowMetadata(id, states)
11
+ const {state} = data
12
+ const client = useClient()
13
+ const toast = useToast()
14
+
15
+ if (loading || error) {
16
+ if (error) {
17
+ console.error(error)
18
+ }
19
+
20
+ return null
21
+ }
22
+
23
+ if (!state) {
24
+ return null
25
+ }
26
+
27
+ const onHandle = (documentId: string, newState: State) => {
28
+ client
29
+ .patch(`workflow-metadata.${documentId}`)
30
+ .set({state: newState.id})
31
+ .commit()
32
+ .then(() => {
33
+ props.onComplete()
34
+ toast.push({
35
+ status: 'success',
36
+ title: `Document demoted to ${newState.title}`,
37
+ })
38
+ })
39
+ .catch((err) => {
40
+ props.onComplete()
41
+ console.error(err)
42
+ toast.push({
43
+ status: 'error',
44
+ title: `Document demotion failed`,
45
+ })
46
+ })
47
+ }
48
+
49
+ const currentStateIndex = states.findIndex((s) => s.id === state.id)
50
+ const prevState = states[currentStateIndex - 1]
51
+
52
+ if (!prevState) {
53
+ return null
54
+ }
55
+
56
+ return {
57
+ icon: ArrowLeftIcon,
58
+ label: `Demote`,
59
+ title: `Demote State to "${prevState.title}"`,
60
+ onHandle: () => onHandle(id, prevState),
61
+ }
62
+ }
@@ -0,0 +1,62 @@
1
+ import {ArrowRightIcon} from '@sanity/icons'
2
+ import {useToast} from '@sanity/ui'
3
+ import {DocumentActionProps, useClient} from 'sanity'
4
+ import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
5
+
6
+ import {State} from '../types'
7
+
8
+ export function PromoteAction(props: DocumentActionProps, states: State[]) {
9
+ const {id} = props
10
+ const {data, loading, error} = useWorkflowMetadata(id, states)
11
+ const {state} = data
12
+ const client = useClient()
13
+ const toast = useToast()
14
+
15
+ if (loading || error) {
16
+ if (error) {
17
+ console.error(error)
18
+ }
19
+
20
+ return null
21
+ }
22
+
23
+ if (!state) {
24
+ return null
25
+ }
26
+
27
+ const onHandle = (documentId: string, newState: State) => {
28
+ client
29
+ .patch(`workflow-metadata.${documentId}`)
30
+ .set({state: newState.id})
31
+ .commit()
32
+ .then(() => {
33
+ props.onComplete()
34
+ toast.push({
35
+ status: 'success',
36
+ title: `Document promoted to ${newState.title}`,
37
+ })
38
+ })
39
+ .catch((err) => {
40
+ props.onComplete()
41
+ console.error(err)
42
+ toast.push({
43
+ status: 'error',
44
+ title: `Document promotion failed`,
45
+ })
46
+ })
47
+ }
48
+
49
+ const currentStateIndex = states.findIndex((s) => s.id === state.id)
50
+ const nextState = states[currentStateIndex + 1]
51
+
52
+ if (!nextState) {
53
+ return null
54
+ }
55
+
56
+ return {
57
+ icon: ArrowRightIcon,
58
+ label: `Promote`,
59
+ title: `Promote State to "${nextState.title}"`,
60
+ onHandle: () => onHandle(id, nextState),
61
+ }
62
+ }
@@ -0,0 +1,61 @@
1
+ import PropTypes from 'prop-types'
2
+ import React from 'react'
3
+ import {EyeOpenIcon} from '@sanity/icons'
4
+
5
+ import {inferMetadataState, useWorkflowMetadata} from '../../lib/workflow'
6
+ import RequestReviewWizard from '../../components/RequestReviewWizard'
7
+
8
+ export function RequestReviewAction(props) {
9
+ const [showWizardDialog, setShowWizardDialog] = React.useState(false)
10
+ const metadata = useWorkflowMetadata(props.id, inferMetadataState(props))
11
+ const {state} = metadata.data
12
+
13
+ if (!props.draft || state === 'inReview' || state === 'approved') {
14
+ return null
15
+ }
16
+
17
+ const onHandle = () => {
18
+ if (!showWizardDialog) {
19
+ setShowWizardDialog(true)
20
+ }
21
+ }
22
+
23
+ const onSend = (assignees) => {
24
+ setShowWizardDialog(false)
25
+
26
+ if (assignees.length === 0) {
27
+ metadata.clearAssignees()
28
+ } else {
29
+ metadata.setAssignees(assignees)
30
+ }
31
+
32
+ metadata.setState('inReview')
33
+ props.onComplete()
34
+ }
35
+
36
+ const onClose = () => setShowWizardDialog(false)
37
+
38
+ return {
39
+ dialog: showWizardDialog && {
40
+ type: 'popover',
41
+ content: (
42
+ <RequestReviewWizard
43
+ metadata={metadata.data}
44
+ onClose={onClose}
45
+ onSend={onSend}
46
+ />
47
+ ),
48
+ onClose: props.onComplete,
49
+ },
50
+ disabled: showWizardDialog,
51
+ icon: EyeOpenIcon,
52
+ label: 'Request review',
53
+ onHandle,
54
+ }
55
+ }
56
+
57
+ RequestReviewAction.propTypes = {
58
+ draft: PropTypes.object,
59
+ id: PropTypes.string,
60
+ onComplete: PropTypes.func,
61
+ }
@@ -0,0 +1,21 @@
1
+ import {ApproveAction} from './ApproveAction'
2
+ import {DeleteAction} from './DeleteAction'
3
+ import {DiscardChangesAction} from './DiscardChangesAction'
4
+ import {PublishAction} from './PublishAction'
5
+ import {RequestChangesAction} from './RequestChangesAction'
6
+ import {RequestReviewAction} from './RequestReviewAction'
7
+ import {SyncAction} from './SyncAction'
8
+ import {UnpublishAction} from './Unpublish'
9
+
10
+ export function resolveWorkflowActions(/* docInfo */) {
11
+ return [
12
+ SyncAction,
13
+ RequestReviewAction,
14
+ ApproveAction,
15
+ RequestChangesAction,
16
+ PublishAction,
17
+ UnpublishAction,
18
+ DiscardChangesAction,
19
+ DeleteAction,
20
+ ]
21
+ }
@@ -0,0 +1,31 @@
1
+ import {DocumentBadgeDescription, DocumentBadgeProps} from 'sanity'
2
+ import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
3
+
4
+ import {State} from '../types'
5
+
6
+ export function StateBadge(
7
+ props: DocumentBadgeProps,
8
+ states: State[]
9
+ ): DocumentBadgeDescription | null {
10
+ const {id} = props
11
+ const {data, loading, error} = useWorkflowMetadata(id, states)
12
+ const {state} = data
13
+
14
+ if (loading || error) {
15
+ if (error) {
16
+ console.error(error)
17
+ }
18
+
19
+ return null
20
+ }
21
+
22
+ if (!state) {
23
+ return null
24
+ }
25
+
26
+ return {
27
+ label: state.title,
28
+ title: state.title,
29
+ color: state?.color,
30
+ }
31
+ }
@@ -0,0 +1,39 @@
1
+ import React from 'react'
2
+ import {Box, Flex, Text} from '@sanity/ui'
3
+ import {UserAvatar} from 'sanity'
4
+
5
+ import {User} from '../../types'
6
+
7
+ type AvatarGroupProps = {
8
+ users: User[]
9
+ max?: number
10
+ }
11
+
12
+ export default function AvatarGroup(props: AvatarGroupProps) {
13
+ const {users, max = 3} = props
14
+
15
+ const len = users?.length
16
+ const visibleUsers = React.useMemo(
17
+ () => users.slice(0, max),
18
+ [users]
19
+ ) as User[]
20
+
21
+ if (!users?.length) {
22
+ return null
23
+ }
24
+
25
+ return (
26
+ <Flex align="center">
27
+ {visibleUsers.map((user) => (
28
+ <Box key={user.id} style={{marginRight: -5}}>
29
+ <UserAvatar user={user} />
30
+ </Box>
31
+ ))}
32
+ {len > max && (
33
+ <Box paddingLeft={2}>
34
+ <Text size={1}>+{len - max}</Text>
35
+ </Box>
36
+ )}
37
+ </Flex>
38
+ )
39
+ }
@@ -0,0 +1,27 @@
1
+ import React from 'react'
2
+ import {Button} from '@sanity/ui'
3
+ import {EditIcon} from '@sanity/icons'
4
+ import {useRouter} from 'sanity/router'
5
+
6
+ type EditButtonProps = {
7
+ id: string
8
+ type: string
9
+ }
10
+
11
+ export default function EditButton(props: EditButtonProps) {
12
+ const {id, type} = props
13
+ const {navigateIntent} = useRouter()
14
+
15
+ return (
16
+ <Button
17
+ // eslint-disable-next-line react/jsx-no-bind
18
+ onClick={() => navigateIntent('edit', {id, type})}
19
+ mode="ghost"
20
+ fontSize={1}
21
+ padding={2}
22
+ tabIndex={-1}
23
+ icon={EditIcon}
24
+ text="Edit"
25
+ />
26
+ )
27
+ }