V3
This commit is contained in:
parent
f0df90f9b7
commit
407eb21fc4
@ -70,19 +70,6 @@ module.exports = class ArticlesDBApi {
|
||||
|
||||
|
||||
|
||||
await articles.setAffiliate_links(data.affiliate_links || [], {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await articles.setMedia_assets(data.media_assets || [], {
|
||||
transaction,
|
||||
});
|
||||
|
||||
await articles.setArticle_bundle_items(data.article_bundle_items || [], {
|
||||
transaction,
|
||||
});
|
||||
|
||||
|
||||
|
||||
await FileDBApi.replaceRelationFiles(
|
||||
{
|
||||
@ -214,25 +201,9 @@ module.exports = class ArticlesDBApi {
|
||||
|
||||
await articles.update(updatePayload, {transaction});
|
||||
|
||||
|
||||
|
||||
// Article source-content updates intentionally skip generated many-to-many
|
||||
// affiliate/media/bundle sync. Those joins are handled later in Export Studio.
|
||||
|
||||
|
||||
|
||||
if (data.affiliate_links !== undefined) {
|
||||
await articles.setAffiliate_links(data.affiliate_links, { transaction });
|
||||
}
|
||||
|
||||
if (data.media_assets !== undefined) {
|
||||
await articles.setMedia_assets(data.media_assets, { transaction });
|
||||
}
|
||||
|
||||
if (data.article_bundle_items !== undefined) {
|
||||
await articles.setArticle_bundle_items(data.article_bundle_items, { transaction });
|
||||
}
|
||||
|
||||
|
||||
|
||||
await FileDBApi.replaceRelationFiles(
|
||||
{
|
||||
belongsTo: db.articles.getTableName(),
|
||||
@ -347,22 +318,6 @@ module.exports = class ArticlesDBApi {
|
||||
});
|
||||
|
||||
|
||||
output.affiliate_links = await articles.getAffiliate_links({
|
||||
transaction
|
||||
});
|
||||
|
||||
|
||||
output.media_assets = await articles.getMedia_assets({
|
||||
transaction
|
||||
});
|
||||
|
||||
|
||||
output.article_bundle_items = await articles.getArticle_bundle_items({
|
||||
transaction
|
||||
});
|
||||
|
||||
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@ -382,32 +337,10 @@ module.exports = class ArticlesDBApi {
|
||||
offset = currentPage * limit;
|
||||
|
||||
let include = [
|
||||
|
||||
|
||||
{
|
||||
model: db.affiliate_links,
|
||||
as: 'affiliate_links',
|
||||
required: false,
|
||||
},
|
||||
|
||||
{
|
||||
model: db.media_assets,
|
||||
as: 'media_assets',
|
||||
required: false,
|
||||
},
|
||||
|
||||
{
|
||||
model: db.article_bundle_items,
|
||||
as: 'article_bundle_items',
|
||||
required: false,
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
model: db.file,
|
||||
as: 'featured_images',
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
if (filter) {
|
||||
@ -533,76 +466,6 @@ module.exports = class ArticlesDBApi {
|
||||
|
||||
|
||||
|
||||
if (filter.affiliate_links) {
|
||||
const searchTerms = filter.affiliate_links.split('|');
|
||||
|
||||
include = [
|
||||
{
|
||||
model: db.affiliate_links,
|
||||
as: 'affiliate_links_filter',
|
||||
required: searchTerms.length > 0,
|
||||
where: searchTerms.length > 0 ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } },
|
||||
{
|
||||
original_url: {
|
||||
[Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` }))
|
||||
}
|
||||
}
|
||||
]
|
||||
} : undefined
|
||||
},
|
||||
...include,
|
||||
]
|
||||
}
|
||||
|
||||
if (filter.media_assets) {
|
||||
const searchTerms = filter.media_assets.split('|');
|
||||
|
||||
include = [
|
||||
{
|
||||
model: db.media_assets,
|
||||
as: 'media_assets_filter',
|
||||
required: searchTerms.length > 0,
|
||||
where: searchTerms.length > 0 ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } },
|
||||
{
|
||||
file_name: {
|
||||
[Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` }))
|
||||
}
|
||||
}
|
||||
]
|
||||
} : undefined
|
||||
},
|
||||
...include,
|
||||
]
|
||||
}
|
||||
|
||||
if (filter.article_bundle_items) {
|
||||
const searchTerms = filter.article_bundle_items.split('|');
|
||||
|
||||
include = [
|
||||
{
|
||||
model: db.article_bundle_items,
|
||||
as: 'article_bundle_items_filter',
|
||||
required: searchTerms.length > 0,
|
||||
where: searchTerms.length > 0 ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: searchTerms.map(term => Utils.uuid(term)) } },
|
||||
{
|
||||
sort_order: {
|
||||
[Op.or]: searchTerms.map(term => ({ [Op.iLike]: `%${term}%` }))
|
||||
}
|
||||
}
|
||||
]
|
||||
} : undefined
|
||||
},
|
||||
...include,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
if (filter.createdAtRange) {
|
||||
const [start, end] = filter.createdAtRange;
|
||||
|
||||
|
||||
@ -1,272 +1,91 @@
|
||||
import React from 'react';
|
||||
import BaseIcon from '../BaseIcon';
|
||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
GridActionsCellItem,
|
||||
GridRowParams,
|
||||
GridValueGetterParams,
|
||||
} from '@mui/x-data-grid';
|
||||
import ImageField from '../ImageField';
|
||||
import {saveFile} from "../../helpers/fileSaver";
|
||||
import dataFormatter from '../../helpers/dataFormatter'
|
||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
||||
import ListActionsPopover from '../ListActionsPopover';
|
||||
import React from 'react'
|
||||
import { GridRowParams } from '@mui/x-data-grid'
|
||||
import ListActionsPopover from '../ListActionsPopover'
|
||||
import { hasPermission } from '../../helpers/userPermissions'
|
||||
|
||||
import {hasPermission} from "../../helpers/userPermissions";
|
||||
type Params = (id: string) => void
|
||||
|
||||
type Params = (id: string) => void;
|
||||
export const loadColumns = async (onDelete: Params, _entityName: string, user) => {
|
||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_ARTICLES')
|
||||
|
||||
export const loadColumns = async (
|
||||
onDelete: Params,
|
||||
entityName: string,
|
||||
|
||||
user
|
||||
|
||||
) => {
|
||||
async function callOptionsApi(entityName: string) {
|
||||
|
||||
if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return [];
|
||||
|
||||
try {
|
||||
const data = await axios(`/${entityName}/autocomplete?limit=100`);
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_ARTICLES')
|
||||
|
||||
return [
|
||||
|
||||
{
|
||||
field: 'title',
|
||||
headerName: 'Title',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'slug',
|
||||
headerName: 'Slug',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'status',
|
||||
headerName: 'Status',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'excerpt',
|
||||
headerName: 'Excerpt',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'content_html',
|
||||
headerName: 'ContentHTML',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'content_markdown',
|
||||
headerName: 'ContentMarkdown',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'featured_images',
|
||||
headerName: 'FeaturedImages',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
editable: false,
|
||||
sortable: false,
|
||||
renderCell: (params: GridValueGetterParams) => (
|
||||
<ImageField
|
||||
name={'Avatar'}
|
||||
image={params?.row?.featured_images}
|
||||
className='w-24 h-24 mx-auto lg:w-6 lg:h-6'
|
||||
/>
|
||||
),
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'source_url',
|
||||
headerName: 'SourceURL',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'published_at',
|
||||
headerName: 'PublishedAt',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
|
||||
editable: hasUpdatePermission,
|
||||
|
||||
type: 'dateTime',
|
||||
valueGetter: (params: GridValueGetterParams) =>
|
||||
new Date(params.row.published_at),
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'affiliate_links',
|
||||
headerName: 'AffiliateLinks',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
editable: false,
|
||||
sortable: false,
|
||||
type: 'singleSelect',
|
||||
valueFormatter: ({ value }) =>
|
||||
dataFormatter.affiliate_linksManyListFormatter(value).join(', '),
|
||||
renderEditCell: (params) => (
|
||||
<DataGridMultiSelect {...params} entityName={'affiliate_links'}/>
|
||||
),
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'media_assets',
|
||||
headerName: 'MediaAssets',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
editable: false,
|
||||
sortable: false,
|
||||
type: 'singleSelect',
|
||||
valueFormatter: ({ value }) =>
|
||||
dataFormatter.media_assetsManyListFormatter(value).join(', '),
|
||||
renderEditCell: (params) => (
|
||||
<DataGridMultiSelect {...params} entityName={'media_assets'}/>
|
||||
),
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'article_bundle_items',
|
||||
headerName: 'ArticleBundleItems',
|
||||
flex: 1,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
|
||||
editable: false,
|
||||
sortable: false,
|
||||
type: 'singleSelect',
|
||||
valueFormatter: ({ value }) =>
|
||||
dataFormatter.article_bundle_itemsManyListFormatter(value).join(', '),
|
||||
renderEditCell: (params) => (
|
||||
<DataGridMultiSelect {...params} entityName={'article_bundle_items'}/>
|
||||
),
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
minWidth: 30,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
getActions: (params: GridRowParams) => {
|
||||
|
||||
return [
|
||||
<div key={params?.row?.id}>
|
||||
<ListActionsPopover
|
||||
onDelete={onDelete}
|
||||
itemId={params?.row?.id}
|
||||
pathEdit={`/articles/articles-edit/?id=${params?.row?.id}`}
|
||||
pathView={`/articles/articles-view/?id=${params?.row?.id}`}
|
||||
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
|
||||
/>
|
||||
</div>,
|
||||
]
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
return [
|
||||
{
|
||||
field: 'title',
|
||||
headerName: 'Title',
|
||||
flex: 1,
|
||||
minWidth: 180,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
editable: hasUpdatePermission,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
headerName: 'Status',
|
||||
flex: 0.6,
|
||||
minWidth: 120,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
editable: hasUpdatePermission,
|
||||
},
|
||||
{
|
||||
field: 'excerpt',
|
||||
headerName: 'Excerpt',
|
||||
flex: 1.2,
|
||||
minWidth: 220,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
editable: hasUpdatePermission,
|
||||
},
|
||||
{
|
||||
field: 'content_html',
|
||||
headerName: 'HTML Content',
|
||||
flex: 1.2,
|
||||
minWidth: 220,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
editable: hasUpdatePermission,
|
||||
},
|
||||
{
|
||||
field: 'content_markdown',
|
||||
headerName: 'Meta Paste',
|
||||
flex: 1,
|
||||
minWidth: 180,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
editable: hasUpdatePermission,
|
||||
},
|
||||
{
|
||||
field: 'source_url',
|
||||
headerName: 'Source URL',
|
||||
flex: 1,
|
||||
minWidth: 180,
|
||||
filterable: false,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
editable: hasUpdatePermission,
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
minWidth: 30,
|
||||
headerClassName: 'datagrid--header',
|
||||
cellClassName: 'datagrid--cell',
|
||||
getActions: (params: GridRowParams) => [
|
||||
<div key={params?.row?.id}>
|
||||
<ListActionsPopover
|
||||
onDelete={onDelete}
|
||||
itemId={params?.row?.id}
|
||||
pathEdit={`/articles/articles-edit/?id=${params?.row?.id}`}
|
||||
pathView={`/articles/articles-view/?id=${params?.row?.id}`}
|
||||
hasUpdatePermission={hasUpdatePermission}
|
||||
/>
|
||||
</div>,
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -34,13 +34,13 @@ const ArticlesTablesPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
const [filters] = useState([{label: 'Title', title: 'title'},{label: 'Slug', title: 'slug'},{label: 'Excerpt', title: 'excerpt'},{label: 'ContentHTML', title: 'content_html'},{label: 'ContentMarkdown', title: 'content_markdown'},{label: 'SourceURL', title: 'source_url'},
|
||||
|
||||
|
||||
{label: 'PublishedAt', title: 'published_at', date: 'true'},
|
||||
|
||||
{label: 'AffiliateLinks', title: 'affiliate_links'},{label: 'MediaAssets', title: 'media_assets'},{label: 'ArticleBundleItems', title: 'article_bundle_items'},
|
||||
{label: 'Status', title: 'status', type: 'enum', options: ['draft','published','archived']},
|
||||
const [filters] = useState([
|
||||
{ label: 'Title', title: 'title' },
|
||||
{ label: 'Status', title: 'status', type: 'enum', options: ['draft', 'published', 'archived'] },
|
||||
{ label: 'Excerpt', title: 'excerpt' },
|
||||
{ label: 'HTML Content', title: 'content_html' },
|
||||
{ label: 'Meta Paste', title: 'content_markdown' },
|
||||
{ label: 'Source URL', title: 'source_url' },
|
||||
]);
|
||||
|
||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ARTICLES');
|
||||
@ -94,7 +94,7 @@ const ArticlesTablesPage = () => {
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||
|
||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/articles/articles-new'} color='info' label='New Item'/>}
|
||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/articles/articles-new'} color='info' label='New Article'/>}
|
||||
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
|
||||
@ -1,691 +1,128 @@
|
||||
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
|
||||
import { mdiChartTimelineVariant } from '@mdi/js'
|
||||
import Head from 'next/head'
|
||||
import React, { ReactElement } from 'react'
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import BaseButton from '../../components/BaseButton'
|
||||
import BaseButtons from '../../components/BaseButtons'
|
||||
import BaseDivider from '../../components/BaseDivider'
|
||||
import CardBox from '../../components/CardBox'
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||
import FormField from '../../components/FormField'
|
||||
import { RichTextField } from '../../components/RichTextField'
|
||||
import SectionMain from '../../components/SectionMain'
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||
import { getPageTitle } from '../../config'
|
||||
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import FormField from '../../components/FormField'
|
||||
import BaseDivider from '../../components/BaseDivider'
|
||||
import BaseButtons from '../../components/BaseButtons'
|
||||
import BaseButton from '../../components/BaseButton'
|
||||
import FormCheckRadio from '../../components/FormCheckRadio'
|
||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'
|
||||
import FormFilePicker from '../../components/FormFilePicker'
|
||||
import FormImagePicker from '../../components/FormImagePicker'
|
||||
import { SwitchField } from '../../components/SwitchField'
|
||||
|
||||
import { SelectField } from '../../components/SelectField'
|
||||
import { SelectFieldMany } from "../../components/SelectFieldMany";
|
||||
import {RichTextField} from "../../components/RichTextField";
|
||||
|
||||
import { create } from '../../stores/articles/articlesSlice'
|
||||
import { useAppDispatch } from '../../stores/hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
import moment from 'moment';
|
||||
|
||||
const initialValues = {
|
||||
|
||||
|
||||
title: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
slug: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
status: 'draft',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
excerpt: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
content_html: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
content_markdown: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
featured_images: [],
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
source_url: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
published_at: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
affiliate_links: [],
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
media_assets: [],
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
article_bundle_items: [],
|
||||
|
||||
|
||||
title: '',
|
||||
status: 'draft',
|
||||
excerpt: '',
|
||||
content_html: '',
|
||||
content_markdown: '',
|
||||
source_url: '',
|
||||
}
|
||||
|
||||
const getErrorMessage = (error: any) => {
|
||||
if (!error) return 'Article could not be saved. Please check the form and try again.'
|
||||
if (typeof error === 'string') return error
|
||||
if (error.message) return error.message
|
||||
if (error.error) return error.error
|
||||
return 'Article could not be saved. Please check the form and try again.'
|
||||
}
|
||||
|
||||
const ArticlesNew = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
const [submitError, setSubmitError] = useState('')
|
||||
|
||||
|
||||
|
||||
const handleSubmit = async (data, { setSubmitting }) => {
|
||||
setSubmitError('')
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(create(data))
|
||||
await router.push('/articles/articles-list')
|
||||
try {
|
||||
await dispatch(create(data)).unwrap()
|
||||
await router.push('/articles/articles-list')
|
||||
} catch (error) {
|
||||
console.error('Article create failed:', error)
|
||||
setSubmitError(getErrorMessage(error))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('New Item')}</title>
|
||||
<title>{getPageTitle('New Article')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
||||
{''}
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='New Article' main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
initialValues={
|
||||
|
||||
initialValues
|
||||
|
||||
}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Title"
|
||||
>
|
||||
<Field
|
||||
name="title"
|
||||
placeholder="Title"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="Slug"
|
||||
>
|
||||
<Field
|
||||
name="slug"
|
||||
placeholder="Slug"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Status" labelFor="status">
|
||||
<Field name="status" id="status" component="select">
|
||||
|
||||
<option value="draft">draft</option>
|
||||
|
||||
<option value="published">published</option>
|
||||
|
||||
<option value="archived">archived</option>
|
||||
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Excerpt" hasTextareaHeight>
|
||||
<Field name="excerpt" as="textarea" placeholder="Excerpt" />
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='ContentHTML' hasTextareaHeight>
|
||||
<Field
|
||||
name='content_html'
|
||||
id='content_html'
|
||||
component={RichTextField}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="ContentMarkdown" hasTextareaHeight>
|
||||
<Field name="content_markdown" as="textarea" placeholder="ContentMarkdown" />
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='FeaturedImages'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'articles/featured_images'}
|
||||
name='featured_images'
|
||||
id='featured_images'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormImagePicker}
|
||||
></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="SourceURL"
|
||||
>
|
||||
<Field
|
||||
name="source_url"
|
||||
placeholder="SourceURL"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="PublishedAt"
|
||||
>
|
||||
<Field
|
||||
type="datetime-local"
|
||||
name="published_at"
|
||||
placeholder="PublishedAt"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='AffiliateLinks' labelFor='affiliate_links'>
|
||||
<Field
|
||||
name='affiliate_links'
|
||||
id='affiliate_links'
|
||||
itemRef={'affiliate_links'}
|
||||
options={[]}
|
||||
component={SelectFieldMany}>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='MediaAssets' labelFor='media_assets'>
|
||||
<Field
|
||||
name='media_assets'
|
||||
id='media_assets'
|
||||
itemRef={'media_assets'}
|
||||
options={[]}
|
||||
component={SelectFieldMany}>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label='ArticleBundleItems' labelFor='article_bundle_items'>
|
||||
<Field
|
||||
name='article_bundle_items'
|
||||
id='article_bundle_items'
|
||||
itemRef={'article_bundle_items'}
|
||||
options={[]}
|
||||
component={SelectFieldMany}>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Submit" />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/articles/articles-list')}/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
||||
{({ isSubmitting }) => (
|
||||
<Form>
|
||||
<p className='mb-4 text-sm text-gray-600 dark:text-dark-600'>
|
||||
Add the source article here. Buyer-specific affiliate link replacement happens later in Export Studio.
|
||||
</p>
|
||||
|
||||
{submitError && (
|
||||
<div className='mb-4 rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700'>
|
||||
{submitError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormField label='Title'>
|
||||
<Field name='title' placeholder='Article title' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Status' labelFor='status'>
|
||||
<Field name='status' id='status' component='select'>
|
||||
<option value='draft'>Draft</option>
|
||||
<option value='published'>Published</option>
|
||||
<option value='archived'>Archived</option>
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Excerpt' hasTextareaHeight>
|
||||
<Field name='excerpt' as='textarea' placeholder='Optional short summary for the article list/export screen' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='HTML Content' hasTextareaHeight>
|
||||
<Field name='content_html' id='content_html' component={RichTextField} />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Meta Paste' hasTextareaHeight>
|
||||
<Field
|
||||
name='content_markdown'
|
||||
as='textarea'
|
||||
placeholder='Paste your metadata, notes, markdown, or source details here'
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label='Source URL'>
|
||||
<Field name='source_url' placeholder='Optional source URL' />
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label={isSubmitting ? 'Saving...' : 'Save Article'} disabled={isSubmitting} />
|
||||
<BaseButton type='reset' color='info' outline label='Reset' disabled={isSubmitting} />
|
||||
<BaseButton
|
||||
type='button'
|
||||
color='danger'
|
||||
outline
|
||||
label='Cancel'
|
||||
onClick={() => router.push('/articles/articles-list')}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
@ -694,15 +131,7 @@ const ArticlesNew = () => {
|
||||
}
|
||||
|
||||
ArticlesNew.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated
|
||||
|
||||
permission={'CREATE_ARTICLES'}
|
||||
|
||||
>
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
)
|
||||
return <LayoutAuthenticated permission='CREATE_ARTICLES'>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default ArticlesNew
|
||||
|
||||
@ -34,13 +34,13 @@ const ArticlesTablesPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
||||
const [filters] = useState([{label: 'Title', title: 'title'},{label: 'Slug', title: 'slug'},{label: 'Excerpt', title: 'excerpt'},{label: 'ContentHTML', title: 'content_html'},{label: 'ContentMarkdown', title: 'content_markdown'},{label: 'SourceURL', title: 'source_url'},
|
||||
|
||||
|
||||
{label: 'PublishedAt', title: 'published_at', date: 'true'},
|
||||
|
||||
{label: 'AffiliateLinks', title: 'affiliate_links'},{label: 'MediaAssets', title: 'media_assets'},{label: 'ArticleBundleItems', title: 'article_bundle_items'},
|
||||
{label: 'Status', title: 'status', type: 'enum', options: ['draft','published','archived']},
|
||||
const [filters] = useState([
|
||||
{ label: 'Title', title: 'title' },
|
||||
{ label: 'Status', title: 'status', type: 'enum', options: ['draft', 'published', 'archived'] },
|
||||
{ label: 'Excerpt', title: 'excerpt' },
|
||||
{ label: 'HTML Content', title: 'content_html' },
|
||||
{ label: 'Meta Paste', title: 'content_markdown' },
|
||||
{ label: 'Source URL', title: 'source_url' },
|
||||
]);
|
||||
|
||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_ARTICLES');
|
||||
@ -94,7 +94,7 @@ const ArticlesTablesPage = () => {
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||
|
||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/articles/articles-new'} color='info' label='New Item'/>}
|
||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/articles/articles-new'} color='info' label='New Article'/>}
|
||||
|
||||
<BaseButton
|
||||
className={'mr-3'}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user