Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SLB-448: content moderation on preview #414

Draft
wants to merge 8 commits into
base: release
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,54 @@
"version": "2.0.0",
"tasks": [
{
"label": "Rebuild website",
"command": "pnpm",
"args": ["run", "turbo:build:website"],
"label": "_root: install",
"type": "shell",
"problemMatcher": []
"command": "devbox run 'pnpm i'"
},
{
"label": "_root: turbo:prep",
"type": "shell",
"command": "devbox run 'pnpm run turbo:prep'"
},
{
"label": "_root: turbo:prep:force",
"type": "shell",
"command": "devbox run 'pnpm run turbo:prep:force'"
},
{
"label": "_root: turbo:test",
"type": "shell",
"command": "devbox run 'pnpm run turbo:test'"
},
{
"label": "cms: dev",
"type": "shell",
"command": "devbox run 'cd apps/cms && pnpm run dev'"
},
{
"label": "cms: login",
"type": "shell",
"command": "devbox run 'cd apps/cms && pnpm run login'"
},
{
"label": "preview: dev",
"type": "shell",
"command": "devbox run 'cd apps/preview && pnpm run start'"
},
{
"label": "publisher: dev",
"type": "shell",
"command": "devbox run 'cd apps/publisher && pnpm run dev'"
},
{
"label": "publisher: open",
"type": "shell",
"command": "devbox run 'cd apps/publisher && pnpm run open'"
},
{
"label": "ui: dev",
"type": "shell",
"command": "devbox run 'cd packages/ui && pnpm run dev'"
}
]
}
6 changes: 4 additions & 2 deletions apps/preview/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"start": "node build/index.js"
},
"dependencies": {
"@amazeelabs/react-intl": "^1.1.4",
"@custom/eslint-config": "workspace:*",
"@custom/schema": "workspace:*",
"@custom/ui": "workspace:*",
Expand All @@ -22,8 +23,9 @@
"express-ws": "^5.0.2",
"memorystore": "^1.6.7",
"node-fetch": "^3.3.2",
"react": "^18",
"react-dom": "^18",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.49.2",
"rxjs": "^7.8.1",
"simple-oauth2": "^5.1.0"
},
Expand Down
3 changes: 3 additions & 0 deletions apps/preview/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useEffect } from 'react';
import { retry } from 'rxjs';
import { webSocket } from 'rxjs/webSocket';

import StateTransitionForm from './components/StateTransitionForm';
import { drupalExecutor } from './drupal-executor';

declare global {
Expand All @@ -29,11 +30,13 @@ function App() {
const sub = updates$.subscribe((value) => refresh(value));
return sub.unsubscribe;
}, [refresh]);

return (
<OperationExecutorsProvider
executors={[{ executor: drupalExecutor(window.GRAPHQL_ENDPOINT, false) }]}
>
<Frame>
<StateTransitionForm />
<Preview />
</Frame>
</OperationExecutorsProvider>
Expand Down
252 changes: 252 additions & 0 deletions apps/preview/src/components/StateTransitionForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
'use client';
import { useIntl } from '@amazeelabs/react-intl';
import {
ModerateContentMutation,
PreviewDrupalPageQuery,
} from '@custom/schema';
import { useMutation, useOperation } from '@custom/ui/operations';
import { usePreviewParameters } from '@custom/ui/routes/Preview';
import { useState } from 'react';
import { useForm } from 'react-hook-form';

export default function StateTransitionForm() {
const [showForm, setShowForm] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const { data, trigger, isMutating } = useMutation(ModerateContentMutation);
const { ...previewParams } = usePreviewParameters();

const {
data: previewData,
isLoading: previewIsLoading,
error: previewError,
} = useOperation(PreviewDrupalPageQuery, previewParams);

const intl = useIntl();

const errorMessages =
!isMutating &&
data &&
data.moderateContent?.errors &&
data.moderateContent.errors.length > 0
? data.moderateContent.errors.map((error) => {
return error?.message || '';
})
: null;
const successMessage =
!isMutating && data && data.moderateContent?.result === 'success'
? intl.formatMessage({
defaultMessage: 'The moderation state has been updated.',
id: 'hZBzvI',
})
: null;

if (previewIsLoading) {
return null;
}
if (previewError) {
return (
<div>
<p>
{intl.formatMessage({
defaultMessage:
'The moderation state transition form cannot be displayed because the content failed to load.',
id: 'mFbroE',
})}
</p>
</div>
);
}

return (
<div className="container-page my-10">
<div className="container-content relative">
<button
onClick={() => {
setShowForm(!showForm);
}}
className={`inline-flex items-center gap-2 rounded-md px-4 py-2 font-medium transition-colors duration-200 ${
showForm
? 'bg-gray-600 hover:bg-gray-700'
: 'bg-blue-600 hover:bg-blue-700'
} text-white`}
>
{intl.formatMessage({
defaultMessage: 'Moderate content',
id: '7dfKlm',
})}
<svg
className={`size-4 transition-transform duration-200 ${
showForm ? 'rotate-180' : ''
}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{showForm && (
<div className="absolute left-0 top-12 z-50 max-w-sm rounded-md border bg-white p-3 shadow-lg">
<form
className="space-y-4"
onSubmit={handleSubmit((values) => {
trigger({
contentId: previewParams.id,
// We only support the node entity type for now.
entityType: 'node',
revisionId: previewParams.rid || '',
submittedData: JSON.stringify(values),
accessCredentials: {
previewUserId: previewParams.preview_user_id || '',
previewAccessToken:
previewParams.preview_access_token || '',
},
});
})}
>
{/* Error / success messages after the form has been submittd. */}
{successMessage ? (
<ul>
<li>{successMessage}</li>
</ul>
) : null}
{errorMessages ? (
<ul>
{errorMessages.map((message, index) => (
<li key={index}>{message}</li>
))}
</ul>
) : null}
{/* Errors from the form validation on client side. */}
{errors && (
<ul>
{errors.name && (
<li>
{intl.formatMessage(
{
defaultMessage: '{field} is required.',
id: 'pc9YT+',
},
{
field: intl.formatMessage({
defaultMessage: 'Name',
id: 'HAlOn1',
}),
},
)}
</li>
)}
{errors.comment && (
<li>
{intl.formatMessage(
{
defaultMessage: '{field} is required.',
id: 'pc9YT+',
},
{
field: intl.formatMessage({
defaultMessage: 'Comment',
id: 'LgbKvU',
}),
},
)}
</li>
)}
</ul>
)}
<div className="!mt-0 flex items-center gap-1">
<label className="font-medium">
{intl.formatMessage({
defaultMessage: 'Current state:',
id: 'UiUHpE',
})}
</label>
<span>
{previewData?.preview?.moderationInfo?.currentState?.label}
</span>
</div>
<div className="space-y-1">
<label className="block font-medium">
{intl.formatMessage({
defaultMessage: 'Change to:',
id: 'vcq+xk',
})}
</label>
<select
{...register('new_moderation_state')}
className="block w-full rounded-md border-gray-300"
>
{previewData?.preview?.moderationInfo?.availableStates?.map(
(availableState) => (
<option
key={availableState?.id}
value={availableState?.id}
selected={
availableState?.id ===
previewData?.preview?.moderationInfo?.currentState
?.id || undefined
}
>
{availableState?.label}
</option>
),
)}
</select>
</div>
<div className="space-y-2">
<label className="block font-medium">
{intl.formatMessage({
defaultMessage: 'Name:',
id: 'WF8jKB',
})}
</label>
<input
{...register('name', { required: true })}
className="block w-full rounded-md border-gray-300"
/>
</div>
<div className="space-y-2">
<label className="block font-medium">
{intl.formatMessage({
defaultMessage: 'Comment:',
id: '4FQrdH',
})}
</label>
<textarea
{...register('comment', { required: true })}
className="block min-h-[100px] w-full rounded-md border-gray-300"
/>
</div>
<div>
<button
type="submit"
disabled={isMutating}
className="rounded-md border px-4 py-2"
>
{isMutating
? intl.formatMessage({
defaultMessage: 'Sending...',
id: '82Y7Sa',
})
: intl.formatMessage({
defaultMessage: 'Submit',
id: 'wSZR47',
})}
</button>
</div>
</form>
</div>
)}
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions packages/drupal/custom/custom.info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ dependencies:
- simple_oauth:simple_oauth
- consumers:consumers
- drupal:serialization
- drupal:content_moderation
- silverback_preview_link:silverback_preview_link

'interface translation project': custom
'interface translation server pattern': modules/custom/custom/translations/%language.po
10 changes: 10 additions & 0 deletions packages/drupal/custom/custom.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ services:
custom.menus:
class: Drupal\custom\Menus

custom.content_moderation:
class: Drupal\custom\ContentModeration
arguments:
- '@content_moderation.moderation_information'
- '@content_moderation.state_transition_validation'
- '@current_user'
- '@access_check.silverback_preview_link'
- '@account_switcher'
- '@datetime.time'

custom.entity_language_redirect_subscriber:
class: Drupal\custom\EventSubscriber\EntityLanguageRedirectSubscriber
arguments: ['@language_manager', '@current_route_match']
Expand Down
Loading