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'} /> + +
+ +
{(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 + +
+
+
- +
- )} - {job.running && ( - - RUNNING - - )} + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
-
- -
-
- {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 - -
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
-
-
-
-
- - ))} -
+ + + ))} + + ) : ( + 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 && ( + + + + + + )} +
+ +
+ +
+
+ ))} +
+); + +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; + } + }, }, };