Skip to content

Commit

Permalink
UX-496 Add minimal workflow and badges to sanity studio (#1)
Browse files Browse the repository at this point in the history
* Add minimal workflow and badges to sanity studio

* UX-496 use Visibility Icon

* Add back to draft action
  • Loading branch information
logansparlin authored Apr 21, 2021
1 parent b3371cc commit 38634e9
Show file tree
Hide file tree
Showing 27 changed files with 770 additions and 8 deletions.
286 changes: 284 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
"homepage": "design.sparkpost.com",
"dependencies": {
"@sanity/client": "^2.7.1",
"@sparkpost/design-tokens": "^5.0.0",
"@sparkpost/matchbox": "^5.0.0",
"@sparkpost/matchbox-icons": "^5.0.0",
"groq": "^2.2.6",
"next": "^10.1.3",
"react": "^17.0.2",
Expand Down Expand Up @@ -68,4 +71,4 @@
"typescript": "^4.2.3",
"yargs-parser": "^13.1.2"
}
}
}
7 changes: 7 additions & 0 deletions studio/config/workflow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const types = ['post'];

export const states = [
{ id: 'draft', title: ' Draft ', color: 'warning' },
{ id: 'inReview', title: 'In review', color: 'warning' },
{ id: 'published', title: 'Published', color: 'success' }
];
11 changes: 11 additions & 0 deletions studio/documentActions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import defaultResolver from 'part:@sanity/base/document-actions';
import { types as workflowTypes } from '../config/workflow';
import { resolveWorkflowActions } from './workflow';

export default function resolveDocumentActions(docInfo) {
if (workflowTypes.includes(docInfo.type)) {
return resolveWorkflowActions(docInfo);
}

return defaultResolver(docInfo);
}
23 changes: 23 additions & 0 deletions studio/documentActions/workflow/backToDraft.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Edit } from '@sparkpost/matchbox-icons';
import { inferMetadataState, useWorkflowMetadata } from '../../lib/workflow';

export default function BackToDraftAction(props) {
const metadata = useWorkflowMetadata(props.id, inferMetadataState(props));
const { state } = metadata.data;

if (!props.draft || !state === 'inReview') {
return null;
}

const onHandle = () => {
metadata.setState('draft');
props.onComplete();
};

return {
label: 'Back to Draft',
icon: Edit,
color: 'warning',
onHandle
};
}
41 changes: 41 additions & 0 deletions studio/documentActions/workflow/delete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react'; // importing because we're using dialog
import { Delete } from '@sparkpost/matchbox-icons';
import { inferMetadataState, useWorkflowMetadata } from '../../lib/workflow';
import { useDocumentOperation } from '@sanity/react-hooks';

export default function DeleteAction(props) {
const [showConfirmDialog, setShowConfirmDialog] = React.useState(false);
const metadata = useWorkflowMetadata(props.id, inferMetadataState(props));
const ops = useDocumentOperation(props.id, props.type);

const onHandle = () => {
if (ops.delete.disabled) {
props.onComplete();
return;
}

if (!showConfirmDialog) {
setShowConfirmDialog(true);
return;
}

setShowConfirmDialog(false);
metadata.delete();
ops.delete.execute();
props.onComplete();
};

return {
label: 'Delete',
disabled: ops.delete.disabled,
icon: Delete,
shortcut: 'mod+shift+d',
dialog: showConfirmDialog && {
type: 'confirm',
message: <div>Are you sure you want to delete this post?</div>,
onConfirm: onHandle,
onCancel: () => setShowConfirmDialog(false)
},
onHandle
};
}
44 changes: 44 additions & 0 deletions studio/documentActions/workflow/discardChanges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { useDocumentOperation } from '@sanity/react-hooks';
import ResetIcon from 'part:@sanity/base/reset-icon';

export default function DiscardChangesAction(props) {
const [showConfirmDialog, setShowConfirmDialog] = React.useState(false);
const ops = useDocumentOperation(props.id, props.type);

if (!props.published || props.liveEdit) {
return null;
}

const onHandle = () => {
if (ops.discardChanges.disabled) {
props.onComplete();
return;
}

if (!showConfirmDialog) {
setShowConfirmDialog(true);
return;
}

setShowConfirmDialog(false);
ops.discardChanges.execute();
props.onComplete();
};

const dialog = showConfirmDialog && {
type: 'confirm',
color: 'danger',
onCancel: props.onComplete,
onConfirm: onHandle,
message: <div>Are you sure you want to discard all changes since last published?</div>
};

return {
disabled: ops.discardChanges.disabled,
label: 'Discard changes',
dialog,
icon: ResetIcon,
onHandle
};
}
19 changes: 19 additions & 0 deletions studio/documentActions/workflow/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import ReviewAction from './review';
import PublishAction from './publish';
import DeleteAction from './delete';
import DiscardChangesAction from './discardChanges';
import SyncAction from './sync';
import UnpublishAction from './unpublish';
import BackToDraftAction from './backToDraft';

export function resolveWorkflowActions() {
return [
SyncAction,
ReviewAction,
PublishAction,
BackToDraftAction,
DiscardChangesAction,
DeleteAction,
UnpublishAction
];
}
28 changes: 28 additions & 0 deletions studio/documentActions/workflow/publish.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Publish } from '@sparkpost/matchbox-icons';
import { inferMetadataState, useWorkflowMetadata } from '../../lib/workflow';
import { useDocumentOperation } from '@sanity/react-hooks';

export default function PublishAction(props) {
const ops = useDocumentOperation(props.id, props.type);
const metadata = useWorkflowMetadata(props.id, inferMetadataState(props));
const { state } = metadata.data;

const onHandle = () => {
if (ops.publish.disabled) {
props.onComplete();
return;
}

metadata.setState('published');
ops.publish.execute();
props.onComplete();
};

return {
label: 'Publish',
disabled: props.liveEdit || state === 'published' || ops.publish.disabled,
shortcut: 'mod+shift+p',
icon: Publish,
onHandle
};
}
23 changes: 23 additions & 0 deletions studio/documentActions/workflow/review.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Visibility } from '@sparkpost/matchbox-icons';
import { inferMetadataState, useWorkflowMetadata } from '../../lib/workflow';

export default function ReviewAction(props) {
const metadata = useWorkflowMetadata(props.id, inferMetadataState(props));
const { state } = metadata.data;

if (!props.draft || state === 'inReview') {
return null;
}

const onHandle = () => {
metadata.setState('inReview');
props.onComplete();
};

return {
label: 'Request Review',
shortcut: 'mod+shift+r',
icon: Visibility,
onHandle
};
}
26 changes: 26 additions & 0 deletions studio/documentActions/workflow/sync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useDocumentOperation } from '@sanity/react-hooks';
import { useEffect } from 'react';
import { inferMetadataState, useWorkflowMetadata } from '../../lib/workflow';

export default function SyncAction(props) {
const metadata = useWorkflowMetadata(props.id, inferMetadataState(props));
const ops = useDocumentOperation(props.id, props.type);
const { state } = metadata.data;
const isDraft = Boolean(props.draft);
const isPublished = Boolean(props.published);
const isLoaded = isDraft || isPublished;

useEffect(() => {
if (isLoaded) {
if (state === 'published' && !props.published) {
if (!ops.publish.disabled) ops.publish.execute();
}

if (state !== 'published' && !props.draft) {
if (!ops.unpublish.disabled) ops.unpublish.execute();
}
}
}, [isLoaded]);

return null;
}
44 changes: 44 additions & 0 deletions studio/documentActions/workflow/unpublish.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { useDocumentOperation } from '@sanity/react-hooks';
import { inferMetadataState, useWorkflowMetadata } from '../../lib/workflow';
import UnpublishIcon from 'part:@sanity/base/unpublish-icon';

export default function UnpublishAction(props) {
const [showConfirmDialog, setShowConfirmDialog] = React.useState(false);
const ops = useDocumentOperation(props.id, props.type);
const metadata = useWorkflowMetadata(props.id, inferMetadataState(props));

if (props.liveEdit) {
return null;
}

const onHandle = () => {
if (ops.delete.disabled) {
props.onComplete();
return;
}

if (!showConfirmDialog) {
setShowConfirmDialog(true);
return;
}

setShowConfirmDialog(false);
ops.unpublish.execute();
metadata.setState('draft');
props.onComplete();
};

return {
label: 'Unpublish',
disabled: ops.delete.disabled,
icon: UnpublishIcon,
dialog: showConfirmDialog && {
type: 'confirm',
message: <div>Are you sure you want to unpublish this post?</div>,
onConfirm: onHandle,
onCancel: () => setShowConfirmDialog(false)
},
onHandle
};
}
11 changes: 11 additions & 0 deletions studio/documentBadges/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import defaultResolve from 'part:@sanity/base/document-badges';
import { types as workflowTypes } from '../config/workflow';
import { resolveWorkflowDocumentBadges } from './workflow';

export default function resolveDocumentBadges(docInfo) {
if (workflowTypes.includes(docInfo.type)) {
return resolveWorkflowDocumentBadges(docInfo);
}

defaultResolve(docInfo);
}
45 changes: 45 additions & 0 deletions studio/documentBadges/workflow/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useWorkflowMetadata, inferMetadataState } from '../../lib/workflow';
import { states } from '../../config/workflow';

const publishedBadge = (docInfo) => {
if (!docInfo.published) {
return null;
}

return {
label: 'Published',
title: 'Published',
color: 'success'
};
};

const WorkflowBadge = (docInfo) => {
const metadata = useWorkflowMetadata(docInfo.id, inferMetadataState(docInfo));
const state = states.find((state) => state.id === metadata.data.state);

if (!state) {
return null;
}

if (docInfo.draft && state.id === 'published') {
return {
label: 'Draft',
title: 'Draft',
color: 'warning'
};
}

if (state.id === 'published') {
return null;
}

return {
label: state.title,
title: state.title,
color: state.color
};
};

export function resolveWorkflowDocumentBadges() {
return [publishedBadge, WorkflowBadge];
}
11 changes: 11 additions & 0 deletions studio/lib/workflow/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function inferMetadataState(props) {
if (!props.draft && !props.published) {
return 'draft';
}

if (props.draft) {
return 'draft';
}

return 'published';
}
2 changes: 2 additions & 0 deletions studio/lib/workflow/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './helpers';
export * from './metadata';
25 changes: 25 additions & 0 deletions studio/lib/workflow/metadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useDocumentOperation, useEditState } from '@sanity/react-hooks';

export function useWorkflowMetadata(id, defaultState) {
const editState = useEditState(`workflow-metadata.${id}`, 'workflow.metadata');
const ops = useDocumentOperation(`workflow-metadata.${id}`, 'workflow.metadata');

const data =
editState && editState.published
? editState.published
: { _id: `workflow-metadata.${id}`, _type: 'workflow.metadata', state: defaultState };

return {
data,
delete: deleteMetadata,
setState
};

function deleteMetadata() {
ops.delete.execute();
}

function setState(state) {
ops.patch.execute([{ setIfMissing: { documentId: id } }, { set: { state } }]);
}
}
Loading

0 comments on commit 38634e9

Please sign in to comment.