Show who created each schedule and start with your own
Org admins see every Express schedule in the organization, so a shared organization mixed everyone's schedules together. The Schedule page now shows "Created by …" on each one and, when others' schedules are listed, switches between Mine and Everyone in the organization. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,8 @@ import { toast } from '../components/Toast'
|
||||
import { describeRule } from '../express/parse'
|
||||
import { locale, t, type Key } from '../i18n'
|
||||
import {
|
||||
archiveSchedule, confirmProduction, isRunSchedule, knowledgeReadiness, listRuns, listSchedules, pauseSchedule, PLAN_ACTIVE, productionStatus, quoteProduction, resumeSchedule,
|
||||
planProgress, productionError, runNow as startRun, updateTopic, type ContentPlan, type RunSummary, type Schedule,
|
||||
archiveSchedule, confirmProduction, isMine, isRunSchedule, knowledgeReadiness, listRuns, listSchedules, scheduleOwners, pauseSchedule, PLAN_ACTIVE, productionStatus, quoteProduction, resumeSchedule,
|
||||
planProgress, productionError, runNow as startRun, updateTopic, type ContentPlan, type Owners, type RunSummary, type Schedule,
|
||||
} from '../express/schedules'
|
||||
|
||||
const STUDIO_URL: string = import.meta.env.VITE_STUDIO_URL
|
||||
@@ -33,25 +33,61 @@ const runError = (code: string) => (RUN_ERRORS.has(code) ? t(`runErr.${code}` as
|
||||
const issueReason = (reason: string) => (ISSUE_REASONS.has(reason) ? t(`issue.${reason}` as Key) : reason)
|
||||
const isNotReady = (r: RunSummary) => r.safe_error_code === 'knowledge_not_ready' || r.has_readiness_issues
|
||||
|
||||
// Which schedules the page starts with; remembered per device.
|
||||
type Scope = 'mine' | 'all'
|
||||
const SCOPE_KEY = 'express.scheduleScope'
|
||||
const savedScope = (): Scope => {
|
||||
try { return localStorage.getItem(SCOPE_KEY) === 'all' ? 'all' : 'mine' } catch { return 'mine' }
|
||||
}
|
||||
|
||||
export function SchedulePage() {
|
||||
const schedules = useQuery({ queryKey: ['schedules'], queryFn: listSchedules, refetchInterval: 30_000 })
|
||||
const owners = useQuery({ queryKey: ['scheduleOwners'], queryFn: scheduleOwners, staleTime: 5 * 60_000 })
|
||||
const [scope, setScope] = useState<Scope>(savedScope)
|
||||
const choose = (next: Scope) => {
|
||||
setScope(next)
|
||||
try { localStorage.setItem(SCOPE_KEY, next) } catch { /* keep it for this visit only */ }
|
||||
}
|
||||
|
||||
const all = schedules.data
|
||||
const mine = all && owners.data ? all.filter(s => isMine(s, owners.data)) : undefined
|
||||
// Members only ever see their own, so the switch appears only when someone else's schedules are listed.
|
||||
const mixed = !!all && !!mine && mine.length < all.length
|
||||
const shown = mixed && scope === 'mine' ? mine : all
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={t('sch.title')} description={t('sch.description')}
|
||||
actions={<Link className="btn primary" to="/brain"><Icon name="plus" />{t('sch.newInstruction')}</Link>} />
|
||||
{mixed && (
|
||||
<div className="result-tabs" role="tablist">
|
||||
<button role="tab" aria-selected={scope === 'mine'} className={scope === 'mine' ? 'active' : undefined} onClick={() => choose('mine')}>
|
||||
{t('sch.mine')} <b>{mine.length}</b>
|
||||
</button>
|
||||
<button role="tab" aria-selected={scope === 'all'} className={scope === 'all' ? 'active' : undefined} onClick={() => choose('all')}>
|
||||
{t('sch.all')} <b>{all.length}</b>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="card">
|
||||
<div className="card-head"><h3>{t('sch.cardTitle')}</h3><Icon name="calendar" /></div>
|
||||
{schedules.isPending && <SkeletonRows label={t('sch.loading')} />}
|
||||
{schedules.isError && <Empty icon="info" title={t('sch.loadFailed')}>{schedules.error.message}</Empty>}
|
||||
{schedules.data?.length === 0 && <Empty icon="calendar" title={t('sch.empty')}>{t('sch.emptyCopy')}</Empty>}
|
||||
{!!schedules.data?.length && <div className="result-list">{schedules.data.map(s => <ScheduleRow key={s.agent.id} schedule={s} />)}</div>}
|
||||
{all?.length === 0 && <Empty icon="calendar" title={t('sch.empty')}>{t('sch.emptyCopy')}</Empty>}
|
||||
{mixed && shown?.length === 0 && <Empty icon="calendar" title={t('sch.emptyMine')}>{t('sch.emptyMineCopy')}</Empty>}
|
||||
{!!shown?.length && <div className="result-list">{shown.map(s => <ScheduleRow key={s.agent.id} schedule={s} owners={owners.data} />)}</div>}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ScheduleRow({ schedule }: { schedule: Schedule }) {
|
||||
function createdBy(schedule: Schedule, owners: Owners) {
|
||||
if (isMine(schedule, owners)) return t('sch.byYou')
|
||||
const name = schedule.agent.created_by_user_id != null ? owners.names.get(schedule.agent.created_by_user_id) : undefined
|
||||
return name ? t('sch.by', { name }) : t('sch.byUnknown')
|
||||
}
|
||||
|
||||
function ScheduleRow({ schedule, owners }: { schedule: Schedule; owners?: Owners }) {
|
||||
const { agent, rule, active } = schedule
|
||||
const queryClient = useQueryClient()
|
||||
const [editing, setEditing] = useState(false)
|
||||
@@ -84,6 +120,7 @@ function ScheduleRow({ schedule }: { schedule: Schedule }) {
|
||||
<span className="rule-icon"><Icon name={ruleIcon(rule)} /></span>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<b>{describeRule(rule)}</b>
|
||||
{owners && <div className="schedule-owner"><small><Icon name="user" />{createdBy(schedule, owners)}</small></div>}
|
||||
<div><small>{production
|
||||
? <ProductionLine agentId={agent.id} active={active} />
|
||||
: t(video ? 'sch.nextVideo' : 'sch.nextResearch', { when: active ? fmt(agent.next_run_at) : t('sch.paused') })}</small></div>
|
||||
|
||||
Reference in New Issue
Block a user