
Security News
The Changelog Podcast: Practical Steps to Stay Safe on npm
Learn the essential steps every developer should take to stay secure on npm and reduce exposure to supply chain attacks.
@rytass/cms-react-components
Advanced tools
A comprehensive collection of production-ready React components for building content management systems. Built with Mezzanine UI design system, these components provide standardized interfaces for article management, workflow controls, permissions, and rich content editing with complete TypeScript support.
npm install @rytass/cms-react-components
# Peer dependencies
npm install @mezzanine-ui/core @mezzanine-ui/react @mezzanine-ui/icons react react-dom react-hook-form dayjs lodash
# or
yarn add @rytass/cms-react-components
// App.tsx
import React from 'react';
import { CalendarConfigProvider } from '@mezzanine-ui/react';
import calendarMethodsDayjs from '@mezzanine-ui/core/calendarMethodsDayjs';
import { DialogProvider, ModalProvider } from '@rytass/cms-react-components';
function App() {
  return (
    <CalendarConfigProvider methods={calendarMethodsDayjs}>
      <DialogProvider>
        <ModalProvider>{/* Your app components */}</ModalProvider>
      </DialogProvider>
    </CalendarConfigProvider>
  );
}
export default App;
A comprehensive data table component with built-in sorting, filtering, pagination, and row actions.
import React from 'react';
import { StandardCMSTable, ArticleStage } from '@rytass/cms-react-components';
import { TableColumn } from '@mezzanine-ui/core/table';
interface Article {
  id: string;
  title: string;
  author: string;
  stage: ArticleStage;
  publishedAt: Date | null;
  updatedAt: Date;
}
const columns: TableColumn<Article>[] = [
  {
    title: 'Title',
    dataIndex: 'title',
    width: 300,
    ellipsis: true,
  },
  {
    title: 'Author',
    dataIndex: 'author',
    width: 150,
  },
  {
    title: 'Stage',
    dataIndex: 'stage',
    width: 120,
    render: (stage: ArticleStage) => <StatusBadge stage={stage} />,
  },
  {
    title: 'Published',
    dataIndex: 'publishedAt',
    width: 150,
    render: (date: Date | null) => (date ? new Date(date).toLocaleDateString() : '-'),
  },
];
function ArticleManagement() {
  const [data, setData] = useState<Article[]>([]);
  const [loading, setLoading] = useState(false);
  const [pagination, setPagination] = useState({
    current: 1,
    pageSize: 20,
    total: 0,
  });
  const handleTableChange = (params: any) => {
    // Handle pagination, sorting, filtering
    fetchArticles(params);
  };
  const handleAction = (action: string, record: Article) => {
    switch (action) {
      case 'edit':
        navigateToEdit(record.id);
        break;
      case 'delete':
        confirmDelete(record);
        break;
      case 'publish':
        publishArticle(record);
        break;
    }
  };
  return (
    <StandardCMSTable<Article>
      columns={columns}
      dataSource={data}
      loading={loading}
      pagination={pagination}
      onChange={handleTableChange}
      onAction={handleAction}
      rowKey="id"
      selectable
      actionColumn={{
        title: 'Actions',
        width: 150,
        actions: record => getActionsForStage(record.stage),
      }}
    />
  );
}
Form submission component with integrated workflow actions and validation.
import React from 'react';
import { useForm } from 'react-hook-form';
import { StandardCMSFormActions, ArticleStage } from '@rytass/cms-react-components';
interface ArticleFormData {
  title: string;
  content: string;
  categoryId: string;
  tags: string[];
  publishDate?: Date;
}
function ArticleForm({ article, onSubmit }: ArticleFormProps) {
  const form = useForm<ArticleFormData>({
    defaultValues: {
      title: article?.title || '',
      content: article?.content || '',
      categoryId: article?.categoryId || '',
      tags: article?.tags || [],
    },
  });
  const handleSave = async (data: ArticleFormData) => {
    await saveArticle(data);
    showSuccess('Article saved as draft');
  };
  const handleSubmitForReview = async (data: ArticleFormData) => {
    await submitForReview(data);
    showSuccess('Article submitted for review');
  };
  const handlePublish = async (data: ArticleFormData) => {
    await publishArticle(data);
    showSuccess('Article published successfully');
  };
  return (
    <form>
      {/* Form fields */}
      <input {...form.register('title', { required: true })} />
      <textarea {...form.register('content', { required: true })} />
      <StandardCMSFormActions
        form={form}
        stage={article?.stage || ArticleStage.DRAFT}
        permissions={userPermissions}
        onSave={handleSave}
        onSubmit={handleSubmitForReview}
        onPublish={handlePublish}
        disableActions={form.formState.isSubmitting}
        saveButtonText="Save Draft"
        submitButtonText="Submit for Review"
        publishButtonText="Publish Now"
        showScheduleOption
        onSchedule={date => schedulePublication(data, date)}
      />
    </form>
  );
}
import React, { useState } from 'react';
import { DeleteWithdrawModal, useModal } from '@rytass/cms-react-components';
function ArticleActions({ article }: { article: Article }) {
  const { openModal, closeModal } = useModal();
  const handleDelete = () => {
    openModal({
      component: DeleteWithdrawModal,
      props: {
        title: 'Delete Article',
        message: `Are you sure you want to delete "${article.title}"?`,
        itemType: 'article',
        onConfirm: async () => {
          await deleteArticle(article.id);
          closeModal();
          refreshList();
        },
        onCancel: closeModal,
        severity: 'error',
      },
    });
  };
  const handleWithdraw = () => {
    openModal({
      component: DeleteWithdrawModal,
      props: {
        title: 'Withdraw Article',
        message: `Withdraw "${article.title}" from publication?`,
        itemType: 'article',
        requireReason: true,
        onConfirm: async (reason: string) => {
          await withdrawArticle(article.id, reason);
          closeModal();
          refreshList();
        },
        onCancel: closeModal,
        severity: 'warning',
      },
    });
  };
  return (
    <div>
      <button onClick={handleDelete}>Delete</button>
      <button onClick={handleWithdraw}>Withdraw</button>
    </div>
  );
}
import React from 'react';
import { RejectModal, useModal } from '@rytass/cms-react-components';
function ReviewActions({ article }: { article: Article }) {
  const { openModal, closeModal } = useModal();
  const handleReject = () => {
    openModal({
      component: RejectModal,
      props: {
        title: 'Reject Article',
        itemName: article.title,
        reasons: [
          'Content needs improvement',
          'Missing required information',
          'Does not meet quality standards',
          'Contains inappropriate content',
          'Other',
        ],
        requireComment: true,
        onConfirm: async (reason: string, comment: string) => {
          await rejectArticle(article.id, { reason, comment });
          closeModal();
          showSuccess('Article rejected');
        },
        onCancel: closeModal,
      },
    });
  };
  return <button onClick={handleReject}>Reject</button>;
}
import React from 'react';
import { VerifyReleaseModal, useModal } from '@rytass/cms-react-components';
function PublishActions({ article }: { article: Article }) {
  const { openModal, closeModal } = useModal();
  const handleVerifyAndRelease = () => {
    openModal({
      component: VerifyReleaseModal,
      props: {
        title: 'Verify and Release Article',
        article: {
          title: article.title,
          author: article.author,
          categories: article.categories,
          lastModified: article.updatedAt,
        },
        checklist: [
          'Content has been reviewed for accuracy',
          'All images have proper attribution',
          'SEO metadata is complete',
          'Links have been verified',
        ],
        requireAllChecks: true,
        showPreview: true,
        previewUrl: `/preview/${article.id}`,
        onConfirm: async (checkedItems: string[]) => {
          await verifyAndRelease(article.id, { checkedItems });
          closeModal();
          showSuccess('Article published successfully');
        },
        onCancel: closeModal,
      },
    });
  };
  return <button onClick={handleVerifyAndRelease}>Verify & Release</button>;
}
import React from 'react';
import { LogsModal, useModal, LogEntry } from '@rytass/cms-react-components';
function ArticleHistory({ article }: { article: Article }) {
  const { openModal, closeModal } = useModal();
  const handleViewLogs = async () => {
    const logs = await fetchArticleLogs(article.id);
    openModal({
      component: LogsModal,
      props: {
        title: `History: ${article.title}`,
        logs: logs.map(
          (log): LogEntry => ({
            id: log.id,
            timestamp: log.createdAt,
            user: log.user.name,
            action: log.action,
            details: log.details,
            changes: log.changes,
            version: log.version,
          }),
        ),
        showDiff: true,
        onRevert: async (logId: string) => {
          await revertToVersion(article.id, logId);
          closeModal();
          refreshArticle();
        },
        onClose: closeModal,
      },
    });
  };
  return <button onClick={handleViewLogs}>View History</button>;
}
List view component for displaying content cards or items.
import React from 'react';
import { StandardCMSList } from '@rytass/cms-react-components';
interface ListItem {
  id: string;
  title: string;
  thumbnail: string;
  description: string;
  status: string;
}
function ContentList() {
  const [items, setItems] = useState<ListItem[]>([]);
  const [selectedIds, setSelectedIds] = useState<string[]>([]);
  const handleSelect = (ids: string[]) => {
    setSelectedIds(ids);
  };
  const handleBulkAction = (action: string) => {
    switch (action) {
      case 'delete':
        bulkDelete(selectedIds);
        break;
      case 'archive':
        bulkArchive(selectedIds);
        break;
    }
  };
  return (
    <StandardCMSList<ListItem>
      items={items}
      selectedIds={selectedIds}
      onSelect={handleSelect}
      renderItem={item => (
        <ContentCard
          key={item.id}
          title={item.title}
          thumbnail={item.thumbnail}
          description={item.description}
          status={item.status}
          onClick={() => navigateToDetail(item.id)}
        />
      )}
      bulkActions={[
        { label: 'Delete', value: 'delete', severity: 'error' },
        { label: 'Archive', value: 'archive' },
      ]}
      onBulkAction={handleBulkAction}
      emptyMessage="No content found"
      loading={loading}
    />
  );
}
Tab navigation component for organizing content sections.
import React, { useState } from 'react';
import { StandardCMSTabs } from '@rytass/cms-react-components';
function ArticleEditor() {
  const [activeTab, setActiveTab] = useState('content');
  const tabs = [
    {
      key: 'content',
      label: 'Content',
      badge: hasUnsavedChanges ? '"' : undefined,
    },
    {
      key: 'metadata',
      label: 'Metadata',
    },
    {
      key: 'seo',
      label: 'SEO',
      disabled: !article.published,
    },
    {
      key: 'history',
      label: 'History',
      badge: versionCount,
    },
  ];
  return (
    <StandardCMSTabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} variant="contained" fullWidth>
      {activeTab === 'content' && <ContentEditor />}
      {activeTab === 'metadata' && <MetadataForm />}
      {activeTab === 'seo' && <SEOSettings />}
      {activeTab === 'history' && <VersionHistory />}
    </StandardCMSTabs>
  );
}
import { havePermission, ArticlesPermissions } from '@rytass/cms-react-components';
function ArticleActions({ article, userPermissions }: Props) {
  const canEdit = havePermission(userPermissions, ArticlesPermissions.UpdateArticleInDraft);
  const canDelete = havePermission(userPermissions, ArticlesPermissions.DeleteArticleInDraft);
  const canSubmit = havePermission(userPermissions, ArticlesPermissions.SubmitPutBackArticle);
  const canApprove = havePermission(userPermissions, ArticlesPermissions.ApproveRejectArticle);
  return (
    <div>
      {canEdit && <button onClick={handleEdit}>Edit</button>}
      {canDelete && <button onClick={handleDelete}>Delete</button>}
      {canSubmit && <button onClick={handleSubmit}>Submit</button>}
      {canApprove && <button onClick={handleApprove}>Approve</button>}
    </div>
  );
}
import { ArticleStage, ArticleTableActions, ArticleTableActionsType } from '@rytass/cms-react-components';
const getTableActions = (permissions: string[]): ArticleTableActionsType => {
  const actions: ArticleTableActionsType = {};
  // Draft stage actions
  actions[ArticleStage.DRAFT] = [];
  if (havePermission(permissions, ArticlesPermissions.UpdateArticleInDraft)) {
    actions[ArticleStage.DRAFT].push(ArticleTableActions.Update);
  }
  if (havePermission(permissions, ArticlesPermissions.SubmitPutBackArticle)) {
    actions[ArticleStage.DRAFT].push(ArticleTableActions.Submit);
  }
  // Reviewing stage actions
  actions[ArticleStage.REVIEWING] = [];
  if (havePermission(permissions, ArticlesPermissions.ApproveRejectArticle)) {
    actions[ArticleStage.REVIEWING].push(ArticleTableActions.Review);
  }
  if (havePermission(permissions, ArticlesPermissions.SubmitPutBackArticle)) {
    actions[ArticleStage.REVIEWING].push(ArticleTableActions.PutBack);
  }
  // Released stage actions
  actions[ArticleStage.RELEASED] = [];
  if (havePermission(permissions, ArticlesPermissions.UpdateArticleInReleased)) {
    actions[ArticleStage.RELEASED].push(ArticleTableActions.Update);
  }
  if (havePermission(permissions, ArticlesPermissions.WithdrawArticleInReleased)) {
    actions[ArticleStage.RELEASED].push(ArticleTableActions.Withdraw);
  }
  return actions;
};
The CMS components support a complete article lifecycle with the following stages:
import { ArticleStage } from '@rytass/cms-react-components';
// Article lifecycle stages
const stages = {
  [ArticleStage.DRAFT]: 'Draft - Initial creation state',
  [ArticleStage.REVIEWING]: 'Under Review - Pending approval',
  [ArticleStage.VERIFIED]: 'Verified - Approved but not published',
  [ArticleStage.SCHEDULED]: 'Scheduled - Set for future publication',
  [ArticleStage.RELEASED]: 'Published - Live and public',
  [ArticleStage.UNKNOWN]: 'Unknown - Error or undefined state',
};
// Stage transitions
function getNextStage(currentStage: ArticleStage): ArticleStage | null {
  switch (currentStage) {
    case ArticleStage.DRAFT:
      return ArticleStage.REVIEWING;
    case ArticleStage.REVIEWING:
      return ArticleStage.VERIFIED;
    case ArticleStage.VERIFIED:
      return ArticleStage.RELEASED;
    default:
      return null;
  }
}
import { useDialog } from '@rytass/cms-react-components';
function MyComponent() {
  const { openDialog, closeDialog } = useDialog();
  const handleConfirmAction = () => {
    openDialog({
      title: 'Confirm Action',
      children: 'Are you sure you want to proceed?',
      severity: 'warning',
      confirmText: 'Proceed',
      cancelText: 'Cancel',
      resolve: (confirmed: boolean) => {
        if (confirmed) {
          performAction();
        }
        closeDialog();
      },
    });
  };
  return <button onClick={handleConfirmAction}>Perform Action</button>;
}
import { useModal } from '@rytass/cms-react-components';
function MyComponent() {
  const { openModal, closeModal, isOpen } = useModal();
  const handleOpenCustomModal = () => {
    openModal({
      component: CustomModal,
      props: {
        title: 'Custom Modal',
        data: someData,
        onSave: result => {
          handleSave(result);
          closeModal();
        },
        onCancel: closeModal,
      },
    });
  };
  return (
    <div>
      <button onClick={handleOpenCustomModal}>Open Modal</button>
      {isOpen && <div>Modal is open</div>}
    </div>
  );
}
import { MezzanineProvider, createTheme } from '@mezzanine-ui/react';
import { StandardCMSTable } from '@rytass/cms-react-components';
const customTheme = createTheme({
  palette: {
    primary: '#1976d2',
    secondary: '#dc004e',
  },
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
  },
});
function ThemedApp() {
  return (
    <MezzanineProvider theme={customTheme}>
      <StandardCMSTable {...tableProps} />
    </MezzanineProvider>
  );
}
// custom-cms-styles.scss
@import '@mezzanine-ui/core/styles';
.custom-cms-table {
  .mzn-table {
    &__header {
      background-color: var(--mzn-color-surface-variant);
    }
    &__row {
      &:hover {
        background-color: var(--mzn-color-action-hover);
      }
    }
  }
}
.custom-form-actions {
  .mzn-button-group {
    gap: 8px;
    .mzn-button {
      min-width: 120px;
    }
  }
}
import {
  StandardCMSTableProps,
  StandardCMSFormActionsProps,
  ArticleStage,
  ArticleTableActions,
  ArticlesPermissions,
  LogEntry,
  DialogConfigType,
  ModalConfigType,
} from '@rytass/cms-react-components';
// Custom article type
interface CustomArticle {
  id: string;
  title: string;
  content: string;
  stage: ArticleStage;
  author: {
    id: string;
    name: string;
    email: string;
  };
  categories: string[];
  tags: string[];
  metadata: Record<string, any>;
  createdAt: Date;
  updatedAt: Date;
  publishedAt: Date | null;
}
// Table props with custom type
const tableProps: StandardCMSTableProps<CustomArticle> = {
  columns: [...],
  dataSource: articles,
  loading: false,
  // ... other props
};
// Form props with validation
interface ArticleFormData {
  title: string;
  content: string;
  categoryId: string;
}
const formProps: StandardCMSFormActionsProps<ArticleFormData> = {
  form: useForm<ArticleFormData>(),
  stage: ArticleStage.DRAFT,
  permissions: userPermissions,
  // ... other props
};
import React, { useMemo, useCallback } from 'react';
import { StandardCMSTable } from '@rytass/cms-react-components';
function OptimizedTable() {
  // Memoize columns to prevent re-renders
  const columns = useMemo(
    () => [
      { title: 'Title', dataIndex: 'title', width: 300 },
      { title: 'Author', dataIndex: 'author', width: 150 },
      // ... more columns
    ],
    [],
  );
  // Use callback for action handlers
  const handleAction = useCallback((action: string, record: any) => {
    // Handle action
  }, []);
  // Virtual scrolling for large datasets
  return (
    <StandardCMSTable
      columns={columns}
      dataSource={data}
      onAction={handleAction}
      virtual
      rowHeight={48}
      visibleRows={20}
    />
  );
}
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { StandardCMSFormActions } from '@rytass/cms-react-components';
function FormWithErrorHandling() {
  const handleError = (error: Error) => {
    console.error('Form error:', error);
    showErrorNotification(error.message);
  };
  return (
    <ErrorBoundary fallback={<ErrorFallback />} onError={handleError}>
      <StandardCMSFormActions {...formProps} onError={handleError} />
    </ErrorBoundary>
  );
}
import { StandardCMSTable } from '@rytass/cms-react-components';
function AccessibleTable() {
  return (
    <StandardCMSTable
      {...tableProps}
      ariaLabel="Articles management table"
      ariaDescribedBy="table-description"
      rowAriaLabel={record => `Article: ${record.title}`}
      actionAriaLabels={{
        edit: 'Edit article',
        delete: 'Delete article',
        publish: 'Publish article',
      }}
    />
  );
}
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { StandardCMSTable } from '@rytass/cms-react-components';
describe('StandardCMSTable', () => {
  const mockData = [
    { id: '1', title: 'Article 1', stage: 'DRAFT' },
    { id: '2', title: 'Article 2', stage: 'PUBLISHED' },
  ];
  it('renders table with data', () => {
    render(<StandardCMSTable columns={columns} dataSource={mockData} rowKey="id" />);
    expect(screen.getByText('Article 1')).toBeInTheDocument();
    expect(screen.getByText('Article 2')).toBeInTheDocument();
  });
  it('handles row actions', async () => {
    const handleAction = jest.fn();
    render(<StandardCMSTable columns={columns} dataSource={mockData} onAction={handleAction} rowKey="id" />);
    const editButton = screen.getByLabelText('Edit article 1');
    fireEvent.click(editButton);
    await waitFor(() => {
      expect(handleAction).toHaveBeenCalledWith('edit', mockData[0]);
    });
  });
});
import { renderHook, act } from '@testing-library/react-hooks';
import { useDialog } from '@rytass/cms-react-components';
describe('useDialog', () => {
  it('opens and closes dialog', () => {
    const { result } = renderHook(() => useDialog());
    act(() => {
      result.current.openDialog({
        title: 'Test Dialog',
        children: 'Test content',
      });
    });
    expect(result.current.isOpen).toBe(true);
    act(() => {
      result.current.closeDialog();
    });
    expect(result.current.isOpen).toBe(false);
  });
});
// Before (legacy)
import { Table, Form, Modal } from 'legacy-cms-components';
// After (new)
import { StandardCMSTable, StandardCMSFormActions, useModal } from '@rytass/cms-react-components';
// Update table props
const legacyTableProps = {
  data: articles,
  columns: columnConfig,
  onRowClick: handleClick,
};
const newTableProps = {
  dataSource: articles,
  columns: columnConfig,
  onAction: (action, record) => {
    if (action === 'view') handleClick(record);
  },
};
import { StandardCMSTable } from '@rytass/cms-react-components';
// Enable debug mode for detailed logging
<StandardCMSTable {...tableProps} debug onDebugLog={log => console.log('Table debug:', log)} />;
MIT
FAQs
Rytass Content Management System React Components
We found that @rytass/cms-react-components demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Learn the essential steps every developer should take to stay secure on npm and reduce exposure to supply chain attacks.

Security News
Experts push back on new claims about AI-driven ransomware, warning that hype and sponsored research are distorting how the threat is understood.

Security News
Ruby's creator Matz assumes control of RubyGems and Bundler repositories while former maintainers agree to step back and transfer all rights to end the dispute.