diff --git a/lib/api/routes/userSettingsRoute.js b/lib/api/routes/userSettingsRoute.js
index 03dd0f2..93ee9de 100644
--- a/lib/api/routes/userSettingsRoute.js
+++ b/lib/api/routes/userSettingsRoute.js
@@ -126,4 +126,21 @@ export default async function userSettingsPlugin(fastify) {
return reply.code(500).send({ error: error.message });
}
});
+
+ fastify.post('/jobs-view-mode', async (request, reply) => {
+ const userId = request.session.currentUser;
+ const { jobs_view_mode } = request.body;
+
+ if (jobs_view_mode !== 'grid' && jobs_view_mode !== 'table') {
+ return reply.code(400).send({ error: 'jobs_view_mode must be "grid" or "table".' });
+ }
+
+ try {
+ upsertSettings({ jobs_view_mode }, userId);
+ return { success: true };
+ } catch (error) {
+ logger.error('Error updating jobs view mode setting', error);
+ return reply.code(500).send({ error: error.message });
+ }
+ });
}
diff --git a/package.json b/package.json
index 86ec636..68b8921 100755
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "fredy",
- "version": "21.2.0",
+ "version": "21.3.0",
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
"scripts": {
"prepare": "husky",
diff --git a/ui/src/assets/news/2.png b/ui/src/assets/news/2.png
new file mode 100644
index 0000000..1bbbdac
Binary files /dev/null and b/ui/src/assets/news/2.png differ
diff --git a/ui/src/assets/news/news.json b/ui/src/assets/news/news.json
index cc563cd..753062d 100644
--- a/ui/src/assets/news/news.json
+++ b/ui/src/assets/news/news.json
@@ -1,11 +1,16 @@
{
- "key": "00e6b81777a275f5a140fc9101cb943810db6a69f6eb3927319c5aee0c876515",
+ "key": "00e6b81777a275f5a140fc9101cb943810db6a69f6eb3927319c5aee0c876221",
"content":
[
{
"title": "Table overview for listings",
"text": "Thanks to https://github.com/datenwurm, we now have a table overview for listings. If you decide to use the table view, the decision will be stored.",
"media": "1.png"
+ },
+ {
+ "title": "Table overview for jobs",
+ "text": "Based on datenwurm's, work, I created a table overview for jobs. If you decide to use the table view, the decision will be stored.",
+ "media": "2.png"
}
]
}
diff --git a/ui/src/components/grid/jobs/JobGrid.jsx b/ui/src/components/grid/jobs/JobGrid.jsx
index 2f165a6..8eb6d0a 100644
--- a/ui/src/components/grid/jobs/JobGrid.jsx
+++ b/ui/src/components/grid/jobs/JobGrid.jsx
@@ -21,6 +21,7 @@ import {
Empty,
Radio,
RadioGroup,
+ Tooltip,
} from '@douyinfe/semi-ui-19';
import {
IconAlertTriangle,
@@ -35,6 +36,8 @@ import {
IconArrowUp,
IconArrowDown,
IconHome,
+ IconGridView,
+ IconList,
} from '@douyinfe/semi-icons';
import { useNavigate } from 'react-router-dom';
import ListingDeletionModal from '../../ListingDeletionModal.jsx';
@@ -42,6 +45,7 @@ import { useActions, useSelector } from '../../../services/state/store.js';
import { xhrDelete, xhrPut, xhrPost } from '../../../services/xhr.js';
import { debounce } from '../../../utils';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
+import JobsTable from '../../table/JobsTable.jsx';
import './JobGrid.less';
@@ -54,6 +58,9 @@ const JobGrid = () => {
const actions = useActions();
const navigate = useNavigate();
+ const userSettings = useSelector((state) => state.userSettings.settings);
+ const viewMode = userSettings?.jobs_view_mode ?? 'grid';
+
const [page, setPage] = useState(1);
const pageSize = 12;
@@ -234,6 +241,27 @@ const JobGrid = () => {
onClick={() => setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))}
title={sortDir === 'asc' ? 'Ascending' : 'Descending'}
/>
+
+
+
+ }
+ theme={viewMode === 'grid' ? 'solid' : 'borderless'}
+ onClick={() => actions.userSettings.setJobsViewMode('grid')}
+ aria-label="Grid view"
+ aria-pressed={viewMode === 'grid'}
+ />
+
+
+ }
+ theme={viewMode === 'table' ? 'solid' : 'borderless'}
+ onClick={() => actions.userSettings.setJobsViewMode('table')}
+ aria-label="Table view"
+ aria-pressed={viewMode === 'table'}
+ />
+
+
{(jobsData?.result || []).length === 0 && (
@@ -244,136 +272,144 @@ const JobGrid = () => {
/>
)}
-
- {(jobsData?.result || []).map((job) => (
-
-
-
-
-
-
- {job.name}
-
+ {viewMode === 'grid' ? (
+
+ {(jobsData?.result || []).map((job) => (
+
+
+
+
+
+
+ {job.name}
+
+
+
+ {job.isOnlyShared && (
+
+
+
+
+
+ )}
+ {job.running && (
+
+ RUNNING
+
+ )}
+
-
- {job.isOnlyShared && (
-
+
+
+
+ {job.numberOfFoundListings || 0}
+
+ Listings
+
+
+
+ {job.provider?.length || 0}
+
+ Providers
+
+
+
+ {job.notificationAdapter?.length || 0}
+
+ Adapters
+
+
+
+
+
+
+
+
+ onJobStatusChanged(job.id, checked)}
+ checked={job.enabled}
+ disabled={job.isOnlyShared}
+ size="small"
+ />
+
+ Active
+
+
+
+
-
+ }
+ disabled={job.isOnlyShared || job.running}
+ onClick={() => onJobRun(job.id)}
+ />
- )}
- {job.running && (
-
- RUNNING
-
- )}
+
+
+ }
+ disabled={job.isOnlyShared}
+ onClick={() => navigate(`/jobs/edit/${job.id}`)}
+ />
+
+
+
+
+ }
+ disabled={job.isOnlyShared}
+ onClick={() => navigate('/jobs/new', { state: { cloneFrom: job.id } })}
+ />
+
+
+
+
+ }
+ disabled={job.isOnlyShared}
+ onClick={() => onListingRemoval(job.id)}
+ />
+
+
+
+
+ }
+ disabled={job.isOnlyShared}
+ onClick={() => onJobRemoval(job.id)}
+ />
+
+
+
-
-
-
-
- {job.numberOfFoundListings || 0}
-
- Listings
-
-
-
- {job.provider.length || 0}
-
- Providers
-
-
-
- {job.notificationAdapter.length || 0}
-
- Adapters
-
-
-
-
-
-
-
-
- onJobStatusChanged(job.id, checked)}
- checked={job.enabled}
- disabled={job.isOnlyShared}
- size="small"
- />
-
- Active
-
-
-
-
-
- }
- disabled={job.isOnlyShared || job.running}
- onClick={() => onJobRun(job.id)}
- />
-
-
-
-
- }
- disabled={job.isOnlyShared}
- onClick={() => navigate(`/jobs/edit/${job.id}`)}
- />
-
-
-
-
- }
- disabled={job.isOnlyShared}
- onClick={() => navigate('/jobs/new', { state: { cloneFrom: job.id } })}
- />
-
-
-
-
- }
- disabled={job.isOnlyShared}
- onClick={() => onListingRemoval(job.id)}
- />
-
-
-
-
- }
- disabled={job.isOnlyShared}
- onClick={() => onJobRemoval(job.id)}
- />
-
-
-
-
-
-
- ))}
-
+
+
+ ))}
+
+ ) : (
+
navigate(`/jobs/edit/${id}`)}
+ onClone={(id) => navigate('/jobs/new', { state: { cloneFrom: id } })}
+ onDeleteListings={onListingRemoval}
+ onDeleteJob={onJobRemoval}
+ onStatusChange={onJobStatusChanged}
+ />
+ )}
{(jobsData?.result || []).length > 0 && jobsData?.totalNumber > 12 && (
(
+
+ {jobs.map((job) => (
+
+
+
+
+
+
+ {job.name}
+
+
+
+
+ {job.numberOfFoundListings || 0}
+
+
+
+
+ {job.provider?.length || 0}
+
+
+
+
+ {job.notificationAdapter?.length || 0}
+
+
+
+ onStatusChange(job.id, checked)}
+ />
+ {job.running && (
+
+ RUNNING
+
+ )}
+ {job.isOnlyShared && (
+
+
+
+
+
+ )}
+
+
+
+
+ }
+ disabled={job.isOnlyShared || job.running}
+ onClick={() => onRun(job.id)}
+ />
+
+
+ }
+ disabled={job.isOnlyShared}
+ onClick={() => onEdit(job.id)}
+ />
+
+
+ }
+ disabled={job.isOnlyShared}
+ onClick={() => onClone(job.id)}
+ />
+
+
+ }
+ disabled={job.isOnlyShared}
+ onClick={() => onDeleteListings(job.id)}
+ />
+
+
+ }
+ disabled={job.isOnlyShared}
+ onClick={() => onDeleteJob(job.id)}
+ />
+
+
+
+ ))}
+
+);
+
+export default JobsTable;
diff --git a/ui/src/components/table/JobsTable.less b/ui/src/components/table/JobsTable.less
new file mode 100644
index 0000000..0ba86c9
--- /dev/null
+++ b/ui/src/components/table/JobsTable.less
@@ -0,0 +1,105 @@
+@import '../../tokens.less';
+
+.jobsTable {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+
+ &__row {
+ display: grid;
+ grid-template-columns: 24px 1fr 80px 80px 80px auto auto;
+ align-items: center;
+ gap: @space-3;
+ padding: 8px 12px;
+ background: @color-elevated;
+ border: 1px solid @color-border;
+ border-radius: @radius-chip;
+ transition: background @transition-fast;
+
+ &:hover {
+ background: #252525;
+ }
+
+ &--inactive {
+ opacity: 0.6;
+ }
+
+ &__dot {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &__indicator {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ background-color: rgba(251, 113, 133, 0.7);
+
+ &--active {
+ background-color: rgba(52, 211, 153, 0.8);
+ }
+ }
+ }
+
+ &__name {
+ font-weight: 600;
+ font-size: @text-sm;
+ color: @color-text;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__stat {
+ font-size: @text-sm;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ white-space: nowrap;
+
+ &--blue {
+ color: @color-blue-text;
+ }
+
+ &--orange {
+ color: @color-orange-text;
+ }
+
+ &--purple {
+ color: @color-purple-text;
+ }
+ }
+
+ &__badges {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ min-width: 0;
+ }
+
+ &__actions {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ }
+
+ @media (max-width: 900px) {
+ grid-template-columns: 24px 1fr 80px auto auto;
+
+ .jobsTable__row__stat--orange,
+ .jobsTable__row__stat--purple {
+ display: none;
+ }
+ }
+
+ @media (max-width: 560px) {
+ grid-template-columns: 24px 1fr auto auto;
+
+ .jobsTable__row__stat--blue {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/ui/src/services/state/store.js b/ui/src/services/state/store.js
index 5de6ec4..a13ebff 100644
--- a/ui/src/services/state/store.js
+++ b/ui/src/services/state/store.js
@@ -335,6 +335,20 @@ export const useFredyState = create(
throw Exception;
}
},
+ async setJobsViewMode(jobs_view_mode) {
+ try {
+ await xhrPost('/api/user/settings/jobs-view-mode', { jobs_view_mode });
+ set((state) => ({
+ userSettings: {
+ ...state.userSettings,
+ settings: { ...state.userSettings.settings, jobs_view_mode },
+ },
+ }));
+ } catch (Exception) {
+ console.error('Error while trying to update jobs view mode setting. Error:', Exception);
+ throw Exception;
+ }
+ },
},
};