Skip to content
Open
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
103 changes: 103 additions & 0 deletions admin/src/components/LocalazyPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';

import type { PanelComponent } from '@strapi/content-manager/strapi-admin';
import { Typography, Toggle } from '@strapi/design-system';
import EntryExclusionService from '../modules/entry-exclusion/services/entry-exclusion-service';

import '../i18n';

const LocalazyPanel: PanelComponent = () => {
const { t } = useTranslation();

const location = useLocation();
const [isExcluded, setIsExcluded] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [contentType, setContentType] = useState<string | null>(null);
const [documentId, setDocumentId] = useState<string | null>(null);

// Extract content type and document ID from URL
useEffect(() => {
const extractEntryInfo = () => {
// URL format: /admin/content-manager/collection-types/{contentType}/{documentId}
const pathParts = location.pathname.split('/');
const contentManagerIndex = pathParts.indexOf('content-manager');

if (contentManagerIndex !== -1 && pathParts[contentManagerIndex + 1] === 'collection-types') {
const extractedContentType = pathParts[contentManagerIndex + 2];
const extractedDocumentId = pathParts[contentManagerIndex + 3];

if (extractedContentType && extractedDocumentId) {
setContentType(extractedContentType);
setDocumentId(extractedDocumentId);
}
}
};

extractEntryInfo();
}, [location.pathname]);

// Load the current exclusion state when we have the entry information
useEffect(() => {
const loadExclusionState = async () => {
if (!contentType || !documentId) {
setIsLoading(false);
return;
}

try {
setIsLoading(true);
const exclusionState = await EntryExclusionService.getEntryExclusion(contentType, documentId);
setIsExcluded(exclusionState);
} catch (error) {
console.error('Failed to load entry exclusion state:', error);
setIsExcluded(false);
} finally {
setIsLoading(false);
}
};

loadExclusionState();
}, [contentType, documentId]);

// Handle toggle change
const handleToggleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const { checked } = event.target;
const value = checked;

if (!contentType || !documentId) {
console.warn('Cannot save exclusion state: missing content type or document ID');
return;
}

try {
await EntryExclusionService.setEntryExclusion(contentType, documentId, value);
setIsExcluded(value);
} catch (error) {
console.error('Failed to save entry exclusion state:', error);
// Revert the toggle if save failed
setIsExcluded(!value);
}
};

return {
title: 'Localazy',
content: (
<div>
<div style={{ marginBottom: '8px' }}>
<Typography>{t('plugin_settings.exclude_from_translation')}</Typography>
</div>
<Toggle
checked={isExcluded}
onLabel='True'
offLabel='False'
disabled={isLoading || !contentType || !documentId}
onChange={handleToggleChange}
/>
</div>
),
};
};

export default LocalazyPanel;
58 changes: 58 additions & 0 deletions admin/src/components/LocalazyStatusColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import EntryExclusionService from '../modules/entry-exclusion/services/entry-exclusion-service';
import '../i18n';

// TODO: define props interface
const LocalazyStatusColumn = ({ data, model }: any) => {
const { t } = useTranslation();

const [isExcluded, setIsExcluded] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const checkStatus = async () => {
if (!data?.documentId || !model) {
setIsLoading(false);
return;
}

try {
const excluded = await EntryExclusionService.getEntryExclusion(model, data.documentId);
setIsExcluded(excluded);
} catch (error) {
console.error('Error checking Localazy status:', error);
setIsExcluded(null);
} finally {
setIsLoading(false);
}
};

checkStatus();
}, [data?.documentId, model]);

if (isLoading) {
return <span style={{ color: '#666', fontSize: '12px' }}>...</span>;
}

if (isExcluded === null) {
return <span style={{ color: '#666', fontSize: '12px' }}>-</span>;
}

return (
<span
style={{
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold',
backgroundColor: isExcluded ? '#ff6b6b' : '#51cf66',
color: 'white',
}}
>
{isExcluded ? t('plugin_settings.status_excluded') : t('plugin_settings.status_included')}
</span>
);
};

export default LocalazyStatusColumn;
160 changes: 160 additions & 0 deletions admin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,166 @@ export default {
})
);
},

async bootstrap(app: any) {
/**
* The bulk action API doesn't support intlLabel, so text is hardcoded here
*/
try {
const contentManagerPlugin = app.getPlugin('content-manager');

if (contentManagerPlugin?.apis?.addEditViewSidePanel) {
const { default: LocalazyPanel } = await import('./components/LocalazyPanel');
const apis = contentManagerPlugin.apis;
apis.addEditViewSidePanel([LocalazyPanel]);
} else {
console.warn('Localazy Plugin: Content Manager "addEditViewSidePanel" API not available');
}

const { default: LocalazyStatusColumn } = await import('./components/LocalazyStatusColumn');

// Register hook to add custom column to list view
app.registerHook('Admin/CM/pages/ListView/inject-column-in-table', (params: any) => {
try {
console.log('Localazy: Hook called with params:', params);

const { displayedHeaders, layout } = params;

// Verify displayedHeaders is an array
if (!Array.isArray(displayedHeaders)) {
return params; // Return original params if structure is unexpected
}

// Add Localazy status column
const localazyStatusColumn = {
name: 'localazy_status',
label: 'Localazy Status',
searchable: false,
sortable: false,
attribute: { type: 'custom' },
cellFormatter: (data: any, header: any, context: any) => {
return React.createElement(LocalazyStatusColumn, {
data,
model: context?.model || '',
});
},
};

const updatedHeaders = [...displayedHeaders, localazyStatusColumn];

return {
displayedHeaders: updatedHeaders,
layout,
};
} catch (error) {
console.error('Localazy: Error in hook:', error);
return params; // Return original params on error
}
});

if (contentManagerPlugin?.apis?.addBulkAction) {
const { useNotification } = await import('@strapi/strapi/admin');
await import('./i18n');
const { useTranslation } = await import('react-i18next');

const ExcludeFromTranslationAction = (props: any) => {
const { documents, model } = props;
const { toggleNotification } = useNotification();
const { t } = useTranslation();

return {
label: t('plugin_settings.bulk_action_exclude_from_translation'),
variant: 'secondary',
onClick: async () => {
try {
toggleNotification({
type: 'info',
message: t('plugin_settings.bulk_action_updating_entries'),
});

const { default: EntryExclusionService } = await import(
'./modules/entry-exclusion/services/entry-exclusion-service'
);

// Extract document IDs from the selected documents
const documentIds = documents.map((doc: any) => doc.documentId);

// Set all selected entries as excluded
await EntryExclusionService.setMultipleEntriesExclusion(model, documentIds, true);

// Show success notification
toggleNotification({
type: 'success',
message: t('plugin_settings.bulk_action_success_message'),
timeout: 5_000,
});
} catch (error) {
console.error('Failed to exclude entries from translation:', error);

// Show error notification
toggleNotification({
type: 'danger',
message: t('plugin_settings.bulk_action_failed_message'),
});
}
},
};
};
ExcludeFromTranslationAction.type = 'exclude-translation';

const IncludeToTranslationAction = ({ documents, model }: any) => {
const { toggleNotification } = useNotification();
const { t } = useTranslation();

return {
label: t('plugin_settings.bulk_action_include_to_translation'),
variant: 'secondary',
onClick: async () => {
try {
toggleNotification({
type: 'info',
message: t('plugin_settings.bulk_action_updating_entries'),
});

const { default: EntryExclusionService } = await import(
'./modules/entry-exclusion/services/entry-exclusion-service'
);

// Extract document IDs from the selected documents
const documentIds = documents.map((doc: any) => doc.documentId);

// Set all selected entries as excluded
await EntryExclusionService.setMultipleEntriesExclusion(model, documentIds, false);

// Show success notification
toggleNotification({
type: 'success',
message: t('plugin_settings.bulk_action_success_message'),
timeout: 5_000,
});
} catch (error) {
console.error('Failed to include entries to translation:', error);

// Show error notification
toggleNotification({
type: 'danger',
message: t('plugin_settings.bulk_action_failed_message'),
});
}
},
};
};

IncludeToTranslationAction.type = 'include-translation';

contentManagerPlugin.apis.addBulkAction([ExcludeFromTranslationAction, IncludeToTranslationAction]);
} else {
console.warn('Localazy Plugin: Content Manager "addBulkAction" API not available');
}
} catch (error) {
console.error('Localazy Plugin: Error adding side panel:', error);
}
},
};

const addMenuLink = (app: any) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createStrapiApiAxiosInstance } from '../../@common/api/strapi-api-base';

const BASE_PATH = '/entry-exclusion';
const axiosInstance = createStrapiApiAxiosInstance();

export default class EntryExclusionService {
/**
* Get entry exclusion state for a specific entry
*/
static async getEntryExclusion(contentType: string, documentId: string): Promise<boolean> {
try {
const result = await axiosInstance.get(`${BASE_PATH}/${contentType}/${documentId}`);
return result.data.isExcluded;
} catch (error) {
console.error('Error getting entry exclusion:', error);
throw error;
}
}

/**
* Set entry exclusion state for a specific entry
*/
static async setEntryExclusion(contentType: string, documentId: string, isExcluded: boolean): Promise<boolean> {
try {
const result = await axiosInstance.put(`${BASE_PATH}/${contentType}/${documentId}`, {
isExcluded,
});
return result.data.isExcluded;
} catch (error) {
console.error('Error setting entry exclusion:', error);
throw error;
}
}

/**
* Set exclusion state for multiple entries
*/
static async setMultipleEntriesExclusion(
contentType: string,
documentIds: string[],
isExcluded: boolean
): Promise<void> {
try {
for (let i = 0; i < documentIds.length; i++) {
const docId = documentIds[i];
await this.setEntryExclusion(contentType, docId, isExcluded);
}
} catch (error) {
console.error('Error setting multiple entries exclusion:', error);
throw error;
}
}

/**
* Get all excluded entries for a content type
*/
static async getContentTypeExclusions(contentType: string): Promise<string[]> {
try {
const result = await axiosInstance.get(`${BASE_PATH}/${contentType}`);
return result.data.excludedIds;
} catch (error) {
console.error('Error getting content type exclusions:', error);
throw error;
}
}
}
Loading