Merge remote-tracking branch 'gitlab/dev' into editor_settings/upload_cover_image

This commit is contained in:
bniwredyc 2023-05-09 14:55:19 +02:00
commit 480d14ed6f
31 changed files with 436 additions and 132 deletions

View File

@ -277,5 +277,6 @@
"user already exist": "user already exists", "user already exist": "user already exists",
"view": "view", "view": "view",
"zine": "zine", "zine": "zine",
"Unnamed draft": "Unnamed draft",
"Publish Settings": "Publish Settings" "Publish Settings": "Publish Settings"
} }

View File

@ -298,5 +298,6 @@
"user already exist": "пользователь уже существует", "user already exist": "пользователь уже существует",
"view": "просмотр", "view": "просмотр",
"zine": "журнал", "zine": "журнал",
"Publish Settings": "Настройки публикации" "Publish Settings": "Настройки публикации",
"Unnamed draft": "Unnamed draft"
} }

View File

@ -368,6 +368,10 @@ img {
} }
} }
[data-float] {
max-width: 50%;
}
[data-float='left'] { [data-float='left'] {
float: left; float: left;
} }

View File

@ -124,6 +124,7 @@ export const FullArticle = (props: ArticleProps) => {
<div class="row"> <div class="row">
<article class="col-md-16 col-lg-14 col-xl-12 offset-md-5"> <article class="col-md-16 col-lg-14 col-xl-12 offset-md-5">
<div class={styles.shoutHeader}> <div class={styles.shoutHeader}>
<Show when={mainTopic()}>
<div class={styles.shoutTopic}> <div class={styles.shoutTopic}>
<a <a
href={getPagePath(router, 'topic', { slug: props.article.mainTopic })} href={getPagePath(router, 'topic', { slug: props.article.mainTopic })}
@ -132,6 +133,7 @@ export const FullArticle = (props: ArticleProps) => {
{mainTopic().title} {mainTopic().title}
</a> </a>
</div> </div>
</Show>
<h1>{props.article.title}</h1> <h1>{props.article.title}</h1>
<Show when={props.article.subtitle}> <Show when={props.article.subtitle}>
@ -237,7 +239,7 @@ export const FullArticle = (props: ArticleProps) => {
<Show when={canEdit()}> <Show when={canEdit()}>
<div class={styles.shoutStatsItem}> <div class={styles.shoutStatsItem}>
<a <a
href={getPagePath(router, 'edit', { shoutSlug: props.article.slug })} href={getPagePath(router, 'edit', { shoutId: props.article.id.toString() })}
class={styles.shoutStatsItemInner} class={styles.shoutStatsItemInner}
> >
<Icon name="edit" class={clsx(styles.icon, styles.iconEdit)} /> <Icon name="edit" class={clsx(styles.icon, styles.iconEdit)} />

View File

@ -0,0 +1,44 @@
.created {
@include font-size(1.2rem);
line-height: 1.5rem;
margin-bottom: 20px;
.icon {
height: 1.2rem;
display: inline-block;
vertical-align: middle;
}
}
.titleContainer {
@include font-size(2.6rem);
line-height: 3.2rem;
margin-bottom: 28px;
}
.title {
font-weight: 700;
}
.actions {
@include font-size(1.2rem);
a {
border: 0;
display: inline-block;
}
a + a {
margin-left: 12px;
}
.deleteLink {
color: #d00820;
}
.publishLink {
color: #2bb452;
}
}

View File

@ -0,0 +1,52 @@
import { clsx } from 'clsx'
import styles from './Draft.module.scss'
import type { Shout } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon'
import { formatDate } from '../../utils'
import formatDateTime from '../../utils/formatDateTime'
import { useLocalize } from '../../context/localize'
import { getPagePath, openPage } from '@nanostores/router'
import { router } from '../../stores/router'
import { useEditorContext } from '../../context/editor'
type Props = {
class?: string
shout: Shout
onPublish: (shout: Shout) => void
onDelete: (shout: Shout) => void
}
export const Draft = (props: Props) => {
const { t } = useLocalize()
const handlePublishLinkClick = (e) => {
e.preventDefault()
props.onPublish(props.shout)
}
const handleDeleteLinkClick = (e) => {
e.preventDefault()
props.onDelete(props.shout)
}
return (
<div class={clsx(props.class)}>
<div class={styles.created}>
<Icon name="pencil-outline" class={styles.icon} /> {formatDate(new Date(props.shout.createdAt))}
&nbsp;{formatDateTime(props.shout.createdAt)()}
</div>
<div class={styles.titleContainer}>
<span class={styles.title}>{props.shout.title || t('Unnamed draft')}</span> {props.shout.subtitle}
</div>
<div class={styles.actions}>
<a href={getPagePath(router, 'edit', { shoutId: props.shout.id.toString() })}>{t('Edit')}</a>
<a href="#" onClick={handlePublishLinkClick} class={styles.publishLink}>
{t('Publish')}
</a>
<a href="#" onClick={handleDeleteLinkClick} class={styles.deleteLink}>
{t('Delete')}
</a>
</div>
</div>
)
}

View File

@ -0,0 +1 @@
export { Draft } from './Draft'

View File

@ -42,6 +42,7 @@ import { useEditorContext } from '../../context/editor'
import { isTextSelection } from '@tiptap/core' import { isTextSelection } from '@tiptap/core'
import type { Doc } from 'yjs/dist/src/utils/Doc' import type { Doc } from 'yjs/dist/src/utils/Doc'
import './Prosemirror.scss' import './Prosemirror.scss'
import { TrailingNode } from './extensions/TrailingNode'
type EditorProps = { type EditorProps = {
shoutId: number shoutId: number
@ -176,7 +177,8 @@ export const Editor = (props: EditorProps) => {
placement: 'left' placement: 'left'
}, },
element: floatingMenuRef.current element: floatingMenuRef.current
}) }),
TrailingNode
] ]
})) }))

View File

@ -2,10 +2,10 @@
left: 0; left: 0;
position: relative; position: relative;
vertical-align: middle; vertical-align: middle;
padding-top: 5px;
button { button {
opacity: 0.3; opacity: 0.3;
vertical-align: text-bottom;
transition: opacity 0.3s ease-in-out; transition: opacity 0.3s ease-in-out;
&:hover { &:hover {
@ -21,3 +21,7 @@
min-width: 64vw; min-width: 64vw;
} }
} }
:global(.tippy-box) {
line-height: 1.3;
}

View File

@ -10,7 +10,7 @@ import { getPagePath } from '@nanostores/router'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
type Props = { type Props = {
shoutSlug: string shoutId: number
} }
export const Panel = (props: Props) => { export const Panel = (props: Props) => {
@ -18,7 +18,7 @@ export const Panel = (props: Props) => {
const { const {
isEditorPanelVisible, isEditorPanelVisible,
wordCounter, wordCounter,
actions: { toggleEditorPanel } actions: { toggleEditorPanel, saveShout, publishShout }
} = useEditorContext() } = useEditorContext()
const containerRef: { current: HTMLElement } = { const containerRef: { current: HTMLElement } = {
@ -37,6 +37,16 @@ export const Panel = (props: Props) => {
} }
}) })
const handleSaveLinkClick = (e) => {
e.preventDefault()
saveShout()
}
const handlePublishLinkClick = (e) => {
e.preventDefault()
publishShout()
}
return ( return (
<aside <aside
ref={(el) => (containerRef.current = el)} ref={(el) => (containerRef.current = el)}
@ -53,10 +63,14 @@ export const Panel = (props: Props) => {
<div class={clsx(styles.actionsHolder, styles.scrolled)}> <div class={clsx(styles.actionsHolder, styles.scrolled)}>
<section> <section>
<p> <p>
<a>{t('Publish')}</a> <a href="#" onClick={handlePublishLinkClick}>
{t('Publish')}
</a>
</p> </p>
<p> <p>
<a>{t('Save draft')}</a> <a href="#" onClick={handleSaveLinkClick}>
{t('Save draft')}
</a>
</p> </p>
</section> </section>
@ -70,7 +84,7 @@ export const Panel = (props: Props) => {
<p> <p>
<a <a
class={styles.linkWithIcon} class={styles.linkWithIcon}
href={getPagePath(router, 'edit', { shoutSlug: props.shoutSlug })} href={getPagePath(router, 'edit', { shoutId: props.shoutId.toString() })}
> >
<Icon name="pencil-outline" class={styles.icon} /> <Icon name="pencil-outline" class={styles.icon} />
{t('Editing')} {t('Editing')}
@ -89,7 +103,7 @@ export const Panel = (props: Props) => {
<a>{t('Invite co-authors')}</a> <a>{t('Invite co-authors')}</a>
</p> </p>
<p> <p>
<a href={getPagePath(router, 'editSettings', { shoutSlug: props.shoutSlug })}> <a href={getPagePath(router, 'editSettings', { shoutId: props.shoutId.toString() })}>
{t('Publication settings')} {t('Publication settings')}
</a> </a>
</p> </p>

View File

@ -0,0 +1,70 @@
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state'
// @ts-ignore
function nodeEqualsType({ types, node }) {
return (Array.isArray(types) && types.includes(node.type)) || node.type === types
}
/**
* Extension based on:
* - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js
* - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts
*/
export interface TrailingNodeOptions {
node: string
notAfter: string[]
}
export const TrailingNode = Extension.create<TrailingNodeOptions>({
name: 'trailingNode',
addOptions() {
return {
node: 'paragraph',
notAfter: ['paragraph']
}
},
addProseMirrorPlugins() {
const plugin = new PluginKey(this.name)
const disabledNodes = Object.entries(this.editor.schema.nodes)
.map(([, value]) => value)
.filter((node) => this.options.notAfter.includes(node.name))
return [
new Plugin({
key: plugin,
appendTransaction: (_, __, state) => {
const { doc, tr, schema } = state
const shouldInsertNodeAtEnd = plugin.getState(state)
const endPosition = doc.content.size
const type = schema.nodes[this.options.node]
if (!shouldInsertNodeAtEnd) {
return
}
return tr.insert(endPosition, type.create())
},
state: {
init: (_, state) => {
const lastNode = state.tr.doc.lastChild
return !nodeEqualsType({ node: lastNode, types: disabledNodes })
},
apply: (tr, value) => {
if (!tr.docChanged) {
return value
}
const lastNode = tr.doc.lastChild
return !nodeEqualsType({ node: lastNode, types: disabledNodes })
}
}
})
]
}
})

View File

@ -12,7 +12,7 @@ import stylesHeader from '../Nav/Header.module.scss'
import { getDescription } from '../../utils/meta' import { getDescription } from '../../utils/meta'
import { FeedArticlePopup } from './FeedArticlePopup' import { FeedArticlePopup } from './FeedArticlePopup'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { openPage } from '@nanostores/router' import { getPagePath, openPage } from '@nanostores/router'
import { router, useRouter } from '../../stores/router' import { router, useRouter } from '../../stores/router'
interface ArticleCardProps { interface ArticleCardProps {
@ -75,7 +75,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
const { title, subtitle } = getTitleAndSubtitle(props.article) const { title, subtitle } = getTitleAndSubtitle(props.article)
const { cover, layout, slug, authors, stat, body } = props.article const { id, cover, layout, slug, authors, stat, body } = props.article
const { changeSearchParam } = useRouter() const { changeSearchParam } = useRouter()
const scrollToComments = (event) => { const scrollToComments = (event) => {
@ -190,9 +190,9 @@ export const ArticleCard = (props: ArticleCardProps) => {
<div class={styles.shoutCardDetailsContent}> <div class={styles.shoutCardDetailsContent}>
<div class={styles.shoutCardDetailsItem}> <div class={styles.shoutCardDetailsItem}>
<button> <a href={getPagePath(router, 'edit', { shoutId: id.toString() })}>
<Icon name="pencil-outline" class={clsx(styles.icon, styles.feedControlIcon)} /> <Icon name="pencil-outline" class={clsx(styles.icon, styles.feedControlIcon)} />
</button> </a>
</div> </div>
<div class={styles.shoutCardDetailsItem}> <div class={styles.shoutCardDetailsItem}>

View File

@ -14,7 +14,6 @@ import { useLocalize } from '../../context/localize'
import { getPagePath, openPage } from '@nanostores/router' import { getPagePath, openPage } from '@nanostores/router'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { useEditorContext } from '../../context/editor' import { useEditorContext } from '../../context/editor'
import { apiClient } from '../../utils/apiClient'
type HeaderAuthProps = { type HeaderAuthProps = {
setIsProfilePopupVisible: (value: boolean) => void setIsProfilePopupVisible: (value: boolean) => void
@ -57,11 +56,12 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
toggleEditorPanel() toggleEditorPanel()
} }
const handleSaveButtonClick = async () => { const handleSaveButtonClick = () => {
const result = await saveShout() saveShout()
if (result) {
openPage(router, 'drafts')
} }
const handlePublishButtonClick = () => {
publishShout()
} }
return ( return (
@ -111,6 +111,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
</> </>
} }
variant={'outline'} variant={'outline'}
onClick={handlePublishButtonClick}
/> />
</div> </div>

View File

@ -0,0 +1,7 @@
.DraftsView {
display: block;
}
.draft {
margin-bottom: 56px;
}

View File

@ -0,0 +1,68 @@
import { clsx } from 'clsx'
import styles from './DraftsView.module.scss'
import { createSignal, For, onMount, Show } from 'solid-js'
import { Draft } from '../../Draft'
import { useSession } from '../../../context/session'
import { Shout } from '../../../graphql/types.gen'
import { apiClient } from '../../../utils/apiClient'
import { useEditorContext } from '../../../context/editor'
import { openPage } from '@nanostores/router'
import { router } from '../../../stores/router'
export const DraftsView = () => {
const { isAuthenticated, isSessionLoaded, user } = useSession()
const [drafts, setDrafts] = createSignal<Shout[]>([])
const loadDrafts = async () => {
const loadedDrafts = await apiClient.getDrafts()
setDrafts(loadedDrafts)
}
onMount(() => {
loadDrafts()
})
const {
actions: { publishShoutById, deleteShout }
} = useEditorContext()
const handleDraftDelete = (shout: Shout) => {
const result = deleteShout(shout.id)
if (result) {
loadDrafts()
}
}
const handleDraftPublish = (shout: Shout) => {
const result = publishShoutById(shout.id)
if (result) {
openPage(router, 'feed')
}
}
return (
<div class={clsx(styles.DraftsView)}>
<Show when={isSessionLoaded()}>
<div class="wide-container">
<div class="row">
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
<For each={drafts()}>
{(draft) => (
<Draft
class={styles.draft}
shout={draft}
onDelete={handleDraftDelete}
onPublish={handleDraftPublish}
/>
)}
</For>
</Show>
</div>
</div>
</div>
</Show>
</div>
)
}

View File

@ -0,0 +1 @@
export { DraftsView } from './DraftsView'

View File

@ -156,6 +156,7 @@
.scrollTopButton { .scrollTopButton {
pointer-events: none; pointer-events: none;
user-select: none;
cursor: pointer; cursor: pointer;
left: 2rem; left: 2rem;
position: sticky; position: sticky;

View File

@ -74,11 +74,6 @@ export const EditView = (props: EditViewProps) => {
if (title) { if (title) {
setFormErrors('title', '') setFormErrors('title', '')
} }
// if (!isSlugChanged()) {
// const slug = translit(title).replaceAll(' ', '-')
// setForm('slug', slug)
// }
} }
const handleSlugInputChange = (e) => { const handleSlugInputChange = (e) => {
@ -254,7 +249,7 @@ export const EditView = (props: EditViewProps) => {
<Modal variant="narrow" name="uploadImage"> <Modal variant="narrow" name="uploadImage">
<UploadModalContent onClose={(value) => handleUploadModalContentCloseSetCover(value)} /> <UploadModalContent onClose={(value) => handleUploadModalContentCloseSetCover(value)} />
</Modal> </Modal>
<Panel shoutSlug={props.shout.slug} /> <Panel shoutId={props.shout.id} />
</> </>
) )
} }

View File

@ -5,6 +5,9 @@ import { Topic } from '../graphql/types.gen'
import { apiClient } from '../utils/apiClient' import { apiClient } from '../utils/apiClient'
import { useLocalize } from './localize' import { useLocalize } from './localize'
import { useSnackbar } from './snackbar' import { useSnackbar } from './snackbar'
import { translit } from '../utils/ru2en'
import { openPage } from '@nanostores/router'
import { router, useRouter } from '../stores/router'
type WordCounter = { type WordCounter = {
characters: number characters: number
@ -28,8 +31,10 @@ type EditorContextType = {
form: ShoutForm form: ShoutForm
formErrors: Partial<ShoutForm> formErrors: Partial<ShoutForm>
actions: { actions: {
saveShout: () => Promise<boolean> saveShout: () => Promise<void>
publishShout: () => Promise<boolean> publishShout: () => Promise<void>
publishShoutById: (shoutId: number) => Promise<void>
deleteShout: (shoutId: number) => Promise<boolean>
toggleEditorPanel: () => void toggleEditorPanel: () => void
countWords: (value: WordCounter) => void countWords: (value: WordCounter) => void
setForm: SetStoreFunction<ShoutForm> setForm: SetStoreFunction<ShoutForm>
@ -45,6 +50,9 @@ export function useEditorContext() {
export const EditorProvider = (props: { children: JSX.Element }) => { export const EditorProvider = (props: { children: JSX.Element }) => {
const { t } = useLocalize() const { t } = useLocalize()
const { page } = useRouter()
const { const {
actions: { showSnackbar } actions: { showSnackbar }
} = useSnackbar() } = useSnackbar()
@ -62,12 +70,59 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value) const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value)
const countWords = (value) => setWordCounter(value) const countWords = (value) => setWordCounter(value)
const saveShout = async () => { const validate = () => {
if (!form.title) { if (!form.title) {
setFormErrors('title', t('Required')) setFormErrors('title', t('Required'))
return false return false
} }
return true
}
const saveShout = async () => {
if (!validate()) {
return
}
try {
const shout = await apiClient.updateArticle({
shoutId: form.shoutId,
shoutInput: {
body: form.body,
topics: form.selectedTopics.map((topic) => topic.slug),
// authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
// community?: InputMaybe<Scalars['Int']>
mainTopic: form.selectedTopics[0]?.slug || 'society',
slug: form.slug,
subtitle: form.subtitle,
title: form.title,
cover: form.coverImageUrl
},
publish: false
})
if (shout.visibility === 'owner') {
openPage(router, 'drafts')
} else {
openPage(router, 'article', { slug: shout.slug })
}
} catch (error) {
console.error('[saveShout]', error)
showSnackbar({ type: 'error', body: t('Error') })
}
}
const publishShout = async () => {
if (!validate()) {
return
}
if (page().route === 'edit') {
const slug = translit(form.title.toLowerCase()).replaceAll(' ', '-')
setForm('slug', slug)
openPage(router, 'editSettings', { shoutId: form.shoutId.toString() })
return
}
try { try {
await apiClient.updateArticle({ await apiClient.updateArticle({
shoutId: form.shoutId, shoutId: form.shoutId,
@ -81,30 +136,34 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
subtitle: form.subtitle, subtitle: form.subtitle,
title: form.title, title: form.title,
cover: form.coverImageUrl cover: form.coverImageUrl
} },
publish: true
}) })
return true openPage(router, 'feed')
} catch (error) { } catch (error) {
console.error(error) console.error('[publishShout]', error)
showSnackbar({ type: 'error', body: t('Error') }) showSnackbar({ type: 'error', body: t('Error') })
return false
} }
} }
const publishShout = async () => { const publishShoutById = async (shoutId: number) => {
try { try {
await apiClient.publishShout({ await apiClient.updateArticle({
slug: form.slug, shoutId,
shoutInput: { publish: true
body: form.body, })
topics: form.selectedTopics.map((topic) => topic.slug),
// authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>> openPage(router, 'feed')
// community?: InputMaybe<Scalars['Int']> } catch (error) {
mainTopic: form.selectedTopics[0]?.slug || '', console.error('[publishShoutById]', error)
slug: form.slug, showSnackbar({ type: 'error', body: t('Error') })
subtitle: form.subtitle,
title: form.title
} }
}
const deleteShout = async (shoutId: number) => {
try {
await apiClient.deleteShout({
shoutId
}) })
return true return true
} catch { } catch {
@ -116,6 +175,8 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
const actions = { const actions = {
saveShout, saveShout,
publishShout, publishShout,
publishShoutById,
deleteShout,
toggleEditorPanel, toggleEditorPanel,
countWords, countWords,
setForm, setForm,

View File

@ -1,8 +1,8 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
mutation UpdateShoutMutation($shoutId: Int!, $shoutInput: ShoutInput!) { mutation UpdateShoutMutation($shoutId: Int!, $shoutInput: ShoutInput, $publish: Boolean) {
updateShout(shout_id: $shoutId, shout_input: $shoutInput) { updateShout(shout_id: $shoutId, shout_input: $shoutInput, publish: $publish) {
error error
shout { shout {
id id
@ -10,6 +10,7 @@ export default gql`
title title
subtitle subtitle
body body
visibility
} }
} }
} }

View File

@ -1,16 +0,0 @@
import { gql } from '@urql/core'
export default gql`
mutation PublishShoutMutation($slug: String!, $shout: ShoutInput!) {
publishShout(slug: $slug, inp: $shout) {
error
shout {
id
slug
title
subtitle
body
}
}
}
`

View File

@ -1,8 +1,8 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
query LoadShoutQuery($slug: String!) { query LoadShoutQuery($slug: String, $shoutId: Int) {
loadShout(slug: $slug) { loadShout(slug: $slug, shout_id: $shoutId) {
id id
title title
subtitle subtitle

View File

@ -173,7 +173,6 @@ export type Mutation = {
follow: Result follow: Result
getSession: AuthResult getSession: AuthResult
markAsRead: Result markAsRead: Result
publishShout: Result
rateUser: Result rateUser: Result
registerUser: AuthResult registerUser: AuthResult
sendLink: Result sendLink: Result
@ -245,11 +244,6 @@ export type MutationMarkAsReadArgs = {
ids: Array<InputMaybe<Scalars['Int']>> ids: Array<InputMaybe<Scalars['Int']>>
} }
export type MutationPublishShoutArgs = {
shout_id: Scalars['Int']
shout_input?: InputMaybe<ShoutInput>
}
export type MutationRateUserArgs = { export type MutationRateUserArgs = {
slug: Scalars['String'] slug: Scalars['String']
value: Scalars['Int'] value: Scalars['Int']
@ -292,8 +286,9 @@ export type MutationUpdateReactionArgs = {
} }
export type MutationUpdateShoutArgs = { export type MutationUpdateShoutArgs = {
publish?: InputMaybe<Scalars['Boolean']>
shout_id: Scalars['Int'] shout_id: Scalars['Int']
shout_input: ShoutInput shout_input?: InputMaybe<ShoutInput>
} }
export type MutationUpdateTopicArgs = { export type MutationUpdateTopicArgs = {
@ -394,7 +389,8 @@ export type QueryLoadRecipientsArgs = {
} }
export type QueryLoadShoutArgs = { export type QueryLoadShoutArgs = {
slug: Scalars['String'] shout_id?: InputMaybe<Scalars['Int']>
slug?: InputMaybe<Scalars['String']>
} }
export type QueryLoadShoutsArgs = { export type QueryLoadShoutsArgs = {

View File

@ -4,7 +4,7 @@ import { apiClient } from '../utils/apiClient'
export const onBeforeRender = async (pageContext: PageContext) => { export const onBeforeRender = async (pageContext: PageContext) => {
const { slug } = pageContext.routeParams const { slug } = pageContext.routeParams
const article = await apiClient.getShout(slug) const article = await apiClient.getShoutBySlug(slug)
const pageProps: PageProps = { article } const pageProps: PageProps = { article }

View File

@ -8,7 +8,7 @@ import { redirectPage } from '@nanostores/router'
export const CreatePage = () => { export const CreatePage = () => {
onMount(async () => { onMount(async () => {
const shout = await apiClient.createArticle({ article: {} }) const shout = await apiClient.createArticle({ article: {} })
redirectPage(router, 'edit', { shoutSlug: shout.slug }) redirectPage(router, 'edit', { shoutId: shout.id.toString() })
}) })
return ( return (

View File

@ -1,40 +1,15 @@
import { createSignal, For, onMount, Show } from 'solid-js'
import { PageLayout } from '../components/_shared/PageLayout' import { PageLayout } from '../components/_shared/PageLayout'
import { useSession } from '../context/session' import { Title } from '@solidjs/meta'
import { Shout } from '../graphql/types.gen' import { useLocalize } from '../context/localize'
import { apiClient } from '../utils/apiClient' import { DraftsView } from '../components/Views/DraftsView'
import { getPagePath } from '@nanostores/router'
import { router } from '../stores/router'
export const DraftsPage = () => { export const DraftsPage = () => {
const { isAuthenticated, isSessionLoaded, user } = useSession() const { t } = useLocalize()
const [drafts, setDrafts] = createSignal<Shout[]>([])
onMount(async () => {
const loadedDrafts = await apiClient.getDrafts()
setDrafts(loadedDrafts)
})
return ( return (
<PageLayout> <PageLayout>
<Show when={isSessionLoaded()}> <Title>{t('Drafts')}</Title>
<div class="wide-container"> <DraftsView />
<div class="row">
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
<Show when={isAuthenticated()} fallback="Давайте авторизуемся">
<For each={drafts()}>
{(draft) => (
<div>
<a href={getPagePath(router, 'edit', { shoutSlug: draft.slug })}>{draft.id}</a>
</div>
)}
</For>
</Show>
</div>
</div>
</div>
</Show>
</PageLayout> </PageLayout>
) )
} }

View File

@ -13,12 +13,12 @@ export const EditPage = () => {
const { page } = useRouter() const { page } = useRouter()
const shoutSlug = createMemo(() => (page().params as Record<'shoutSlug', string>).shoutSlug) const shoutId = createMemo(() => Number((page().params as Record<'shoutId', string>).shoutId))
const [shout, setShout] = createSignal<Shout>(null) const [shout, setShout] = createSignal<Shout>(null)
onMount(async () => { onMount(async () => {
const loadedShout = await apiClient.getShout(shoutSlug()) const loadedShout = await apiClient.getShoutById(shoutId())
setShout(loadedShout) setShout(loadedShout)
}) })

View File

@ -8,8 +8,8 @@ export const ROUTES = {
inbox: '/inbox', inbox: '/inbox',
connect: '/connect', connect: '/connect',
create: '/create', create: '/create',
edit: '/edit/:shoutSlug', edit: '/edit/:shoutId',
editSettings: '/edit/:shoutSlug/settings', editSettings: '/edit/:shoutId/settings',
drafts: '/drafts', drafts: '/drafts',
topics: '/topics', topics: '/topics',
topic: '/topic/:slug', topic: '/topic/:slug',

View File

@ -124,7 +124,7 @@ const addSortedArticles = (articles: Shout[]) => {
} }
export const loadShout = async (slug: string): Promise<void> => { export const loadShout = async (slug: string): Promise<void> => {
const newArticle = await apiClient.getShout(slug) const newArticle = await apiClient.getShoutBySlug(slug)
addArticles([newArticle]) addArticles([newArticle])
const newArticleIndex = sortedArticles().findIndex((s) => s.id === newArticle.id) const newArticleIndex = sortedArticles().findIndex((s) => s.id === newArticle.id)
if (newArticleIndex >= 0) { if (newArticleIndex >= 0) {

View File

@ -52,7 +52,7 @@ import loadRecipients from '../graphql/query/chat-recipients'
import createMessage from '../graphql/mutation/create-chat-message' import createMessage from '../graphql/mutation/create-chat-message'
import updateProfile from '../graphql/mutation/update-profile' import updateProfile from '../graphql/mutation/update-profile'
import updateArticle from '../graphql/mutation/article-update' import updateArticle from '../graphql/mutation/article-update'
import publishShout from '../graphql/mutation/shout-publish' import deleteShout from '../graphql/mutation/article-delete'
type ApiErrorCode = type ApiErrorCode =
| 'unknown' | 'unknown'
@ -250,21 +250,22 @@ export const apiClient = {
}, },
updateArticle: async ({ updateArticle: async ({
shoutId, shoutId,
shoutInput shoutInput,
publish
}: { }: {
shoutId: number shoutId: number
shoutInput: ShoutInput shoutInput?: ShoutInput
publish: boolean
}): Promise<Shout> => { }): Promise<Shout> => {
const response = await privateGraphQLClient.mutation(updateArticle, { shoutId, shoutInput }).toPromise() const response = await privateGraphQLClient
.mutation(updateArticle, { shoutId, shoutInput, publish })
.toPromise()
console.debug('[updateArticle]:', response.data) console.debug('[updateArticle]:', response.data)
return response.data.updateShout.shout return response.data.updateShout.shout
}, },
publishShout: async ({ slug, shoutInput }: { slug: string; shoutInput: ShoutInput }): Promise<Shout> => { deleteShout: async ({ shoutId }: { shoutId: number }): Promise<void> => {
const response = await privateGraphQLClient const response = await privateGraphQLClient.mutation(deleteShout, { shoutId }).toPromise()
.mutation(publishShout, { slug, shout: shoutInput }) console.debug('[deleteShout]:', response)
.toPromise()
console.debug('[publishShout]:', response)
return response.data.publishShout.shout
}, },
getDrafts: async (): Promise<Shout[]> => { getDrafts: async (): Promise<Shout[]> => {
const response = await privateGraphQLClient.query(draftsLoad, {}).toPromise() const response = await privateGraphQLClient.query(draftsLoad, {}).toPromise()
@ -292,15 +293,33 @@ export const apiClient = {
const resp = await publicGraphQLClient.query(authorsLoadBy, options).toPromise() const resp = await publicGraphQLClient.query(authorsLoadBy, options).toPromise()
return resp.data.loadAuthorsBy return resp.data.loadAuthorsBy
}, },
getShout: async (slug: string) => { getShoutBySlug: async (slug: string) => {
const resp = await publicGraphQLClient const resp = await publicGraphQLClient
.query(shoutLoad, { .query(shoutLoad, {
slug slug
}) })
.toPromise() .toPromise()
if (resp.error) console.debug(resp)
if (resp.error) {
console.error(resp)
}
return resp.data.loadShout return resp.data.loadShout
}, },
getShoutById: async (shoutId: number) => {
const resp = await publicGraphQLClient
.query(shoutLoad, {
shoutId
})
.toPromise()
if (resp.error) {
console.error(resp)
}
return resp.data.loadShout
},
getShouts: async (options: LoadShoutsOptions) => { getShouts: async (options: LoadShoutsOptions) => {
const resp = await publicGraphQLClient.query(shoutsLoadBy, { options }).toPromise() const resp = await publicGraphQLClient.query(shoutsLoadBy, { options }).toPromise()