mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-07 08:23:55 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 342533e128 |
@@ -1,38 +1,45 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { CustomField, CustomFieldKey, CustomFields } from 'ontime-types';
|
import { CustomField, CustomFieldKey } from 'ontime-types'; // Removed CustomFields
|
||||||
|
|
||||||
import { apiEntryUrl } from './constants';
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
// Define CustomFieldWithKey for client-side usage
|
||||||
|
export type CustomFieldWithKey = CustomField & { key: CustomFieldKey };
|
||||||
|
|
||||||
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests list of known custom fields
|
* Requests list of known custom fields, sorted by order
|
||||||
*/
|
*/
|
||||||
export async function getCustomFields(): Promise<CustomFields> {
|
export async function getCustomFields(): Promise<CustomFieldWithKey[]> {
|
||||||
const res = await axios.get(customFieldsPath);
|
const res = await axios.get<CustomFieldWithKey[]>(customFieldsPath);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets list of known custom fields
|
* Sets list of known custom fields
|
||||||
|
* Returns the updated list, sorted by order
|
||||||
*/
|
*/
|
||||||
export async function postCustomField(newField: CustomField): Promise<CustomFields> {
|
export async function postCustomField(newField: CustomField): Promise<CustomFieldWithKey[]> {
|
||||||
const res = await axios.post(customFieldsPath, { ...newField });
|
const res = await axios.post<CustomFieldWithKey[]>(customFieldsPath, { ...newField });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Edits single custom field
|
* Edits single custom field
|
||||||
|
* Returns the updated list, sorted by order
|
||||||
*/
|
*/
|
||||||
export async function editCustomField(key: CustomFieldKey, newField: CustomField): Promise<CustomFields> {
|
export async function editCustomField(key: CustomFieldKey, newField: Partial<CustomField>): Promise<CustomFieldWithKey[]> {
|
||||||
const res = await axios.put(`${customFieldsPath}/${key}`, { ...newField });
|
// Ensure newField can include 'order' by using Partial<CustomField>
|
||||||
|
const res = await axios.put<CustomFieldWithKey[]>(`${customFieldsPath}/${key}`, { ...newField });
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes single custom field
|
* Deletes single custom field
|
||||||
|
* Returns the updated list, sorted by order
|
||||||
*/
|
*/
|
||||||
export async function deleteCustomField(key: CustomFieldKey): Promise<CustomFields> {
|
export async function deleteCustomField(key: CustomFieldKey): Promise<CustomFieldWithKey[]> {
|
||||||
const res = await axios.delete(`${customFieldsPath}/${key}`);
|
const res = await axios.delete<CustomFieldWithKey[]>(`${customFieldsPath}/${key}`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { CustomFields } from 'ontime-types';
|
// CustomFields record type is no longer used here
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { CUSTOM_FIELDS } from '../api/constants';
|
import { CUSTOM_FIELDS } from '../api/constants';
|
||||||
import { getCustomFields } from '../api/customFields';
|
import { getCustomFields, CustomFieldWithKey } from '../api/customFields'; // Import CustomFieldWithKey
|
||||||
|
|
||||||
const placeholder: CustomFields = {};
|
const placeholder: CustomFieldWithKey[] = []; // Placeholder is now an empty array
|
||||||
|
|
||||||
export default function useCustomFields() {
|
export default function useCustomFields() {
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
// Explicitly type the useQuery hook
|
||||||
|
const { data, status, isFetching, isError, refetch } = useQuery<CustomFieldWithKey[], Error>({
|
||||||
queryKey: CUSTOM_FIELDS,
|
queryKey: CUSTOM_FIELDS,
|
||||||
queryFn: getCustomFields,
|
queryFn: getCustomFields,
|
||||||
placeholderData: (previousData, _previousQuery) => previousData,
|
placeholderData: (previousData, _previousQuery) => previousData ?? placeholder,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
refetchInterval: queryRefetchIntervalSlow,
|
refetchInterval: queryRefetchIntervalSlow,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react'; // Import useEffect
|
||||||
import { IoAdd } from 'react-icons/io5';
|
import { IoAdd } from 'react-icons/io5';
|
||||||
import { CustomField, CustomFieldKey } from 'ontime-types';
|
import { CustomField, CustomFieldKey } from 'ontime-types';
|
||||||
|
|
||||||
import { deleteCustomField, editCustomField, postCustomField } from '../../../../common/api/customFields';
|
// Import CustomFieldWithKey
|
||||||
|
import { deleteCustomField, editCustomField, postCustomField, CustomFieldWithKey } from '../../../../common/api/customFields';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
import Button from '../../../../common/components/buttons/Button';
|
||||||
import Info from '../../../../common/components/info/Info';
|
import Info from '../../../../common/components/info/Info';
|
||||||
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
||||||
@@ -14,8 +15,61 @@ import CustomFieldEntry from './composite/CustomFieldEntry';
|
|||||||
import CustomFieldForm from './composite/CustomFieldForm';
|
import CustomFieldForm from './composite/CustomFieldForm';
|
||||||
|
|
||||||
export default function CustomFieldSettings() {
|
export default function CustomFieldSettings() {
|
||||||
const { data, refetch } = useCustomFields();
|
const { data, refetch } = useCustomFields(); // data is CustomFieldWithKey[]
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
const [displayedFields, setDisplayedFields] = useState<CustomFieldWithKey[]>([]);
|
||||||
|
const [orderChanged, setOrderChanged] = useState(false); // To track if order has changed for enabling Save button
|
||||||
|
const [isSavingOrder, setIsSavingOrder] = useState(false); // For Save Order button loading state
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize displayedFields with fetched data, ensuring a fresh copy for local manipulation
|
||||||
|
// And sort it initially, as the backend already provides it sorted, but this ensures consistency.
|
||||||
|
setDisplayedFields(data ? [...data].sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity)) : []);
|
||||||
|
// When data is refetched from server (e.g. after save), reset orderChanged flag
|
||||||
|
setOrderChanged(false);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
const handleSaveOrder = async () => {
|
||||||
|
setIsSavingOrder(true);
|
||||||
|
const updatePromises = [];
|
||||||
|
|
||||||
|
// Create a map of original orders for quick lookup
|
||||||
|
const originalFieldsMap = new Map(data.map(f => [f.key, f]));
|
||||||
|
|
||||||
|
for (let i = 0; i < displayedFields.length; i++) {
|
||||||
|
const currentField = displayedFields[i];
|
||||||
|
const originalField = originalFieldsMap.get(currentField.key);
|
||||||
|
|
||||||
|
// The new order is its current index in the displayedFields array
|
||||||
|
const newOrder = i;
|
||||||
|
|
||||||
|
// Check if the effective order has changed, or if it's a new field without an original order yet (though create handles initial order)
|
||||||
|
// or if the field itself is new and not in originalFieldsMap (less likely here as it should have been created)
|
||||||
|
if (!originalField || originalField.order !== newOrder) {
|
||||||
|
// Only update if the order property is actually different
|
||||||
|
// We need to ensure the object passed to editCustomField has all required CustomField props
|
||||||
|
// and the order. The 'key' is passed as the first argument to editCustomField.
|
||||||
|
const fieldToSave: Partial<CustomField> = {
|
||||||
|
label: currentField.label,
|
||||||
|
type: currentField.type,
|
||||||
|
colour: currentField.colour,
|
||||||
|
order: newOrder,
|
||||||
|
};
|
||||||
|
updatePromises.push(editCustomField(currentField.key, fieldToSave));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(updatePromises);
|
||||||
|
setOrderChanged(false);
|
||||||
|
refetch(); // Refetch data from the server to confirm and get fresh state
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving custom field order:", error);
|
||||||
|
// Potentially show an error message to the user
|
||||||
|
} finally {
|
||||||
|
setIsSavingOrder(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleInitiateCreate = () => {
|
const handleInitiateCreate = () => {
|
||||||
setIsAdding(true);
|
setIsAdding(true);
|
||||||
@@ -50,9 +104,14 @@ export default function CustomFieldSettings() {
|
|||||||
<Panel.Card>
|
<Panel.Card>
|
||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
Custom fields
|
Custom fields
|
||||||
<Button onClick={handleInitiateCreate}>
|
<Panel.InlineElements>
|
||||||
New <IoAdd />
|
<Button onClick={handleSaveOrder} variant='primary' disabled={!orderChanged || isSavingOrder} loading={isSavingOrder}>
|
||||||
</Button>
|
Save Order
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleInitiateCreate} disabled={isSavingOrder}>
|
||||||
|
New <IoAdd />
|
||||||
|
</Button>
|
||||||
|
</Panel.InlineElements>
|
||||||
</Panel.SubHeader>
|
</Panel.SubHeader>
|
||||||
<Panel.Divider />
|
<Panel.Divider />
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
@@ -77,16 +136,67 @@ export default function CustomFieldSettings() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{Object.entries(data).map(([key, { colour, label, type }]) => {
|
{displayedFields.map((field, index) => { // Iterate over displayedFields
|
||||||
return (
|
return (
|
||||||
<CustomFieldEntry
|
<CustomFieldEntry
|
||||||
key={key}
|
key={field.key}
|
||||||
fieldKey={key}
|
fieldKey={field.key}
|
||||||
colour={colour}
|
colour={field.colour}
|
||||||
label={label}
|
label={field.label}
|
||||||
type={type}
|
type={field.type}
|
||||||
|
order={field.order} // Pass order
|
||||||
onEdit={handleEditField}
|
onEdit={handleEditField}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
// For reordering
|
||||||
|
isFirst={index === 0}
|
||||||
|
isLast={index === displayedFields.length - 1} // Use displayedFields.length
|
||||||
|
onMove={(direction: 'up' | 'down') => {
|
||||||
|
const newFields = [...displayedFields];
|
||||||
|
const fieldToMove = newFields[index];
|
||||||
|
let neighborIndex = -1;
|
||||||
|
|
||||||
|
if (direction === 'up' && index > 0) {
|
||||||
|
neighborIndex = index - 1;
|
||||||
|
} else if (direction === 'down' && index < newFields.length - 1) {
|
||||||
|
neighborIndex = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (neighborIndex !== -1) {
|
||||||
|
const neighborField = newFields[neighborIndex];
|
||||||
|
|
||||||
|
// Swap order properties
|
||||||
|
const tempOrder = fieldToMove.order;
|
||||||
|
fieldToMove.order = neighborField.order;
|
||||||
|
neighborField.order = tempOrder;
|
||||||
|
|
||||||
|
// Actual swap in the array for immediate UI feedback before potential sort
|
||||||
|
newFields[index] = neighborField;
|
||||||
|
newFields[neighborIndex] = fieldToMove;
|
||||||
|
|
||||||
|
// Sort by the new order values to ensure dense packing if orders were sparse or undefined
|
||||||
|
// This also handles cases where initial orders might not be perfectly sequential.
|
||||||
|
newFields.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity));
|
||||||
|
|
||||||
|
// If orders became non-dense or undefined, re-assign dense orders
|
||||||
|
// This ensures that when we save, we send a clean, sequential order.
|
||||||
|
let orderIsDirty = false;
|
||||||
|
newFields.forEach((f, i) => {
|
||||||
|
if(f.order !== i) {
|
||||||
|
f.order = i;
|
||||||
|
orderIsDirty = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we had to re-assign orders, sort again just in case (though should be sorted)
|
||||||
|
if(orderIsDirty) {
|
||||||
|
newFields.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setDisplayedFields(newFields);
|
||||||
|
setOrderChanged(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
+14
-4
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IoPencil, IoTrash } from 'react-icons/io5';
|
import { IoArrowDown, IoArrowUp, IoPencil, IoTrash } from 'react-icons/io5';
|
||||||
import { CustomField, CustomFieldKey } from 'ontime-types';
|
import { CustomField, CustomFieldKey } from 'ontime-types';
|
||||||
|
|
||||||
import IconButton from '../../../../../common/components/buttons/IconButton';
|
import IconButton from '../../../../../common/components/buttons/IconButton';
|
||||||
@@ -17,15 +17,19 @@ interface CustomFieldEntryProps {
|
|||||||
label: string;
|
label: string;
|
||||||
fieldKey: string;
|
fieldKey: string;
|
||||||
type: 'string' | 'image';
|
type: 'string' | 'image';
|
||||||
onEdit: (key: CustomFieldKey, patch: CustomField) => Promise<void>;
|
order?: number; // Add order
|
||||||
|
onEdit: (key: CustomFieldKey, patch: Partial<CustomField>) => Promise<void>; // Patch can be partial for order updates
|
||||||
onDelete: (key: CustomFieldKey) => Promise<void>;
|
onDelete: (key: CustomFieldKey) => Promise<void>;
|
||||||
|
isFirst: boolean;
|
||||||
|
isLast: boolean;
|
||||||
|
onMove: (direction: 'up' | 'down') => void; // Changed from Promise<void> to void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
||||||
const { colour, label, fieldKey, type, onEdit, onDelete } = props;
|
const { colour, label, fieldKey, type, onEdit, onDelete, isFirst, isLast, onMove } = props;
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
const handleEdit = async (patch: CustomField) => {
|
const handleEdit = async (patch: CustomField) => { // This patch comes from CustomFieldForm, so it's a full CustomField
|
||||||
await onEdit(fieldKey, patch);
|
await onEdit(fieldKey, patch);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
};
|
};
|
||||||
@@ -61,6 +65,12 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
|||||||
</CopyTag>
|
</CopyTag>
|
||||||
</td>
|
</td>
|
||||||
<Panel.InlineElements relation='inner' as='td'>
|
<Panel.InlineElements relation='inner' as='td'>
|
||||||
|
<IconButton variant='ghosted-white' aria-label='Move field up' onClick={() => onMove('up')} disabled={isFirst}>
|
||||||
|
<IoArrowUp />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton variant='ghosted-white' aria-label='Move field down' onClick={() => onMove('down')} disabled={isLast}>
|
||||||
|
<IoArrowDown />
|
||||||
|
</IconButton>
|
||||||
<IconButton variant='ghosted-white' aria-label='Edit entry' onClick={() => setIsEditing(true)}>
|
<IconButton variant='ghosted-white' aria-label='Edit entry' onClick={() => setIsEditing(true)}>
|
||||||
<IoPencil />
|
<IoPencil />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -1,47 +1,51 @@
|
|||||||
import { CustomField, CustomFields, ErrorResponse } from 'ontime-types';
|
import { CustomField, ErrorResponse } from 'ontime-types';
|
||||||
import { getErrorMessage } from 'ontime-utils';
|
import { getErrorMessage } from 'ontime-utils';
|
||||||
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import type { Request, Response } from 'express';
|
import type { Request, Response } from 'express';
|
||||||
|
|
||||||
import { getProjectCustomFields } from '../rundown/rundown.dao.js';
|
import { getProjectCustomFields, CustomFieldWithKey } from '../rundown/rundown.dao.js';
|
||||||
import { createCustomField, editCustomField, deleteCustomField } from '../rundown/rundown.service.js';
|
import { createCustomField, editCustomField, deleteCustomField } from '../rundown/rundown.service.js';
|
||||||
|
|
||||||
import { validateCustomField, validateDeleteCustomField, validateEditCustomField } from './customFields.validation.js';
|
import { validateCustomField, validateDeleteCustomField, validateEditCustomField } from './customFields.validation.js';
|
||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
|
|
||||||
router.get('/', async (_req: Request, res: Response<CustomFields>) => {
|
// Response type changed to CustomFieldWithKey[]
|
||||||
const customFields = getProjectCustomFields();
|
router.get('/', async (_req: Request, res: Response<CustomFieldWithKey[]>) => {
|
||||||
res.status(200).json(customFields);
|
const customFieldsArray = getProjectCustomFields();
|
||||||
|
res.status(200).json(customFieldsArray);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/', validateCustomField, async (req: Request, res: Response<CustomFields | ErrorResponse>) => {
|
// The POST, PUT, DELETE operations in this router return the entire updated CustomFields object.
|
||||||
|
// This will also need to change to return the sorted array if we want consistency.
|
||||||
|
router.post('/', validateCustomField, async (req: Request, res: Response<CustomFieldWithKey[] | ErrorResponse>) => {
|
||||||
try {
|
try {
|
||||||
const newFields = await createCustomField(req.body as CustomField);
|
// req.body will include label, type, colour, and optionally order
|
||||||
res.status(201).send(newFields);
|
const newFieldsArray = await createCustomField(req.body as CustomField);
|
||||||
|
res.status(201).send(newFieldsArray);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
res.status(400).send({ message });
|
res.status(400).send({ message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.put('/:key', validateEditCustomField, async (req: Request, res: Response<CustomFields | ErrorResponse>) => {
|
router.put('/:key', validateEditCustomField, async (req: Request, res: Response<CustomFieldWithKey[] | ErrorResponse>) => {
|
||||||
try {
|
try {
|
||||||
const currentKey = req.params.key;
|
const currentKey = req.params.key;
|
||||||
const { colour, type, label } = req.body;
|
const { colour, type, label, order } = req.body; // order is now included
|
||||||
const newFields = await editCustomField(currentKey, { label, colour, type });
|
const newFieldsArray = await editCustomField(currentKey, { label, colour, type, order });
|
||||||
res.status(200).send(newFields);
|
res.status(200).send(newFieldsArray);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
res.status(400).send({ message });
|
res.status(400).send({ message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete('/:key', validateDeleteCustomField, async (req: Request, res: Response<CustomFields | ErrorResponse>) => {
|
router.delete('/:key', validateDeleteCustomField, async (req: Request, res: Response<CustomFieldWithKey[] | ErrorResponse>) => {
|
||||||
try {
|
try {
|
||||||
const customFields = await deleteCustomField(req.params.key);
|
const customFieldsArray = await deleteCustomField(req.params.key);
|
||||||
res.status(200).send(customFields);
|
res.status(200).send(customFieldsArray);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
res.status(400).send({ message });
|
res.status(400).send({ message });
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const validateCustomField = [
|
|||||||
}),
|
}),
|
||||||
body('type').isIn(['string', 'image']),
|
body('type').isIn(['string', 'image']),
|
||||||
body('colour').isString().trim(),
|
body('colour').isString().trim(),
|
||||||
|
body('order').optional().isNumeric().toInt(),
|
||||||
|
|
||||||
requestValidationFunction,
|
requestValidationFunction,
|
||||||
];
|
];
|
||||||
@@ -28,6 +29,7 @@ export const validateEditCustomField = [
|
|||||||
}),
|
}),
|
||||||
body('type').isIn(['string', 'image']),
|
body('type').isIn(['string', 'image']),
|
||||||
body('colour').isString().trim(),
|
body('colour').isString().trim(),
|
||||||
|
body('order').optional().isNumeric().toInt(),
|
||||||
|
|
||||||
requestValidationFunction,
|
requestValidationFunction,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -82,7 +82,21 @@ let projectCustomFields: CustomFields = {};
|
|||||||
|
|
||||||
export const getCurrentRundown = (): Readonly<Rundown> => cachedRundown;
|
export const getCurrentRundown = (): Readonly<Rundown> => cachedRundown;
|
||||||
export const getRundownMetadata = (): Readonly<RundownMetadata> => rundownMetadata;
|
export const getRundownMetadata = (): Readonly<RundownMetadata> => rundownMetadata;
|
||||||
export const getProjectCustomFields = (): Readonly<CustomFields> => projectCustomFields;
|
|
||||||
|
export type CustomFieldWithKey = CustomField & { key: CustomFieldKey };
|
||||||
|
export const getProjectCustomFields = (): ReadonlyArray<CustomFieldWithKey> => {
|
||||||
|
return Object.entries(projectCustomFields)
|
||||||
|
.map(([key, value]): CustomFieldWithKey => ({ key, ...value }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const orderA = a.order ?? Infinity; // Fields without order go last
|
||||||
|
const orderB = b.order ?? Infinity;
|
||||||
|
if (orderA === orderB) {
|
||||||
|
// Fallback sort by key if order is the same or both undefined
|
||||||
|
return a.key.localeCompare(b.key);
|
||||||
|
}
|
||||||
|
return orderA - orderB;
|
||||||
|
});
|
||||||
|
};
|
||||||
export const getEntryWithId = (entryId: EntryId): OntimeEntry | undefined => cachedRundown.entries[entryId];
|
export const getEntryWithId = (entryId: EntryId): OntimeEntry | undefined => cachedRundown.entries[entryId];
|
||||||
|
|
||||||
type Transaction = {
|
type Transaction = {
|
||||||
@@ -563,9 +577,12 @@ function customFieldAdd(customFields: CustomFields, key: CustomFieldKey, newCust
|
|||||||
label: newCustomField.label,
|
label: newCustomField.label,
|
||||||
type: newCustomField.type,
|
type: newCustomField.type,
|
||||||
colour: newCustomField.colour,
|
colour: newCustomField.colour,
|
||||||
|
order: newCustomField.order, // Add order here
|
||||||
};
|
};
|
||||||
|
|
||||||
return { [key]: newCustomField };
|
// The return value of this function doesn't seem to be critically used for its content,
|
||||||
|
// but to be safe, let's include order here too.
|
||||||
|
return { [key]: { ...newCustomField } };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { customFieldLabelToKey } from 'ontime-utils';
|
|||||||
import { updateRundownData } from '../../stores/runtimeState.js';
|
import { updateRundownData } from '../../stores/runtimeState.js';
|
||||||
import { runtimeService } from '../../services/runtime-service/RuntimeService.js';
|
import { runtimeService } from '../../services/runtime-service/RuntimeService.js';
|
||||||
|
|
||||||
import { createTransaction, customFieldMutation, rundownCache, rundownMutation } from './rundown.dao.js';
|
import { createTransaction, customFieldMutation, rundownCache, rundownMutation, getProjectCustomFields, CustomFieldWithKey } from './rundown.dao.js';
|
||||||
import type { RundownMetadata } from './rundown.types.js';
|
import type { RundownMetadata } from './rundown.types.js';
|
||||||
import { generateEvent, getInsertAfterId, hasChanges } from './rundown.utils.js';
|
import { generateEvent, getInsertAfterId, hasChanges } from './rundown.utils.js';
|
||||||
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
|
import { sendRefetch } from '../../adapters/WebsocketAdapter.js';
|
||||||
@@ -412,7 +412,7 @@ export async function ungroupEntries(blockId: EntryId): Promise<Rundown> {
|
|||||||
* Adds a new custom field to the project
|
* Adds a new custom field to the project
|
||||||
* @throws if the label is missing or invalid
|
* @throws if the label is missing or invalid
|
||||||
*/
|
*/
|
||||||
export async function createCustomField(customField: CustomField): Promise<CustomFields> {
|
export async function createCustomField(customField: CustomField): Promise<ReadonlyArray<CustomFieldWithKey>> {
|
||||||
const key = customFieldLabelToKey(customField.label);
|
const key = customFieldLabelToKey(customField.label);
|
||||||
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
@@ -426,16 +426,27 @@ export async function createCustomField(customField: CustomField): Promise<Custo
|
|||||||
throw new Error('Label already exists');
|
throw new Error('Label already exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assign order if not provided
|
||||||
|
if (customField.order === undefined) {
|
||||||
|
let maxOrder = -1;
|
||||||
|
Object.values(customFields).forEach(cf => {
|
||||||
|
if (cf.order !== undefined && cf.order > maxOrder) {
|
||||||
|
maxOrder = cf.order;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
customField.order = maxOrder + 1;
|
||||||
|
}
|
||||||
|
|
||||||
customFieldMutation.add(customFields, key, customField);
|
customFieldMutation.add(customFields, key, customField);
|
||||||
|
|
||||||
// Adding a custom field has no immediate implications on the rundown
|
// Adding a custom field has no immediate implications on the rundown
|
||||||
const { customFields: resultCustomFields } = commit(false);
|
commit(false); // Persist changes
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
sendRefetch(RefetchKey.CustomFields);
|
sendRefetch(RefetchKey.CustomFields);
|
||||||
});
|
});
|
||||||
|
|
||||||
return resultCustomFields;
|
return getProjectCustomFields(); // Return sorted array
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -446,7 +457,7 @@ export async function createCustomField(customField: CustomField): Promise<Custo
|
|||||||
* @throws if the label is missing or invalid
|
* @throws if the label is missing or invalid
|
||||||
* @throws if the new label already exists
|
* @throws if the new label already exists
|
||||||
*/
|
*/
|
||||||
export async function editCustomField(key: CustomFieldKey, newField: Partial<CustomField>): Promise<CustomFields> {
|
export async function editCustomField(key: CustomFieldKey, newField: Partial<CustomField>): Promise<ReadonlyArray<CustomFieldWithKey>> {
|
||||||
const { customFields, customFieldsMetadata, rundown, commit } = createTransaction({
|
const { customFields, customFieldsMetadata, rundown, commit } = createTransaction({
|
||||||
mutableRundown: true,
|
mutableRundown: true,
|
||||||
mutableCustomFields: true,
|
mutableCustomFields: true,
|
||||||
@@ -470,26 +481,27 @@ export async function editCustomField(key: CustomFieldKey, newField: Partial<Cus
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the custom fields have been removed and there is no processing to be done
|
// the custom fields have been removed and there is no processing to be done
|
||||||
const { rundownMetadata, revision, customFields: resultCustomFields } = commit(false);
|
const { rundownMetadata, revision } = commit(false); // Persist changes
|
||||||
|
|
||||||
// schedule the side effects
|
// schedule the side effects
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
notifyChanges(rundownMetadata, revision, { timer: true, external: true });
|
notifyChanges(rundownMetadata, revision, { timer: true, external: true });
|
||||||
|
sendRefetch(RefetchKey.CustomFields); // Also refetch custom fields explicitly
|
||||||
});
|
});
|
||||||
|
|
||||||
return resultCustomFields;
|
return getProjectCustomFields(); // Return sorted array
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an existing custom field
|
* Deletes an existing custom field
|
||||||
*/
|
*/
|
||||||
export async function deleteCustomField(key: CustomFieldKey): Promise<CustomFields> {
|
export async function deleteCustomField(key: CustomFieldKey): Promise<ReadonlyArray<CustomFieldWithKey>> {
|
||||||
const { customFields, customFieldsMetadata, rundown, commit } = createTransaction({
|
const { customFields, customFieldsMetadata, rundown, commit } = createTransaction({
|
||||||
mutableRundown: true,
|
mutableRundown: true,
|
||||||
mutableCustomFields: true,
|
mutableCustomFields: true,
|
||||||
});
|
});
|
||||||
if (!(key in customFields)) {
|
if (!(key in customFields)) {
|
||||||
return customFields;
|
return getProjectCustomFields(); // Return sorted array even if key not found
|
||||||
}
|
}
|
||||||
|
|
||||||
customFieldMutation.remove(customFields, key);
|
customFieldMutation.remove(customFields, key);
|
||||||
@@ -498,14 +510,15 @@ export async function deleteCustomField(key: CustomFieldKey): Promise<CustomFiel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the custom fields have been removed and there is no processing to be done
|
// the custom fields have been removed and there is no processing to be done
|
||||||
const { rundownMetadata, revision, customFields: resultCustomFields } = commit(false);
|
const { rundownMetadata, revision } = commit(false); // Persist changes
|
||||||
|
|
||||||
// schedule the side effects
|
// schedule the side effects
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
notifyChanges(rundownMetadata, revision, { timer: true, external: true });
|
notifyChanges(rundownMetadata, revision, { timer: true, external: true });
|
||||||
|
sendRefetch(RefetchKey.CustomFields); // Also refetch custom fields explicitly
|
||||||
});
|
});
|
||||||
|
|
||||||
return resultCustomFields;
|
return getProjectCustomFields(); // Return sorted array
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export type CustomField = {
|
|||||||
type: 'string' | 'image';
|
type: 'string' | 'image';
|
||||||
colour: string;
|
colour: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
order?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomFields = Record<CustomFieldKey, CustomField>;
|
export type CustomFields = Record<CustomFieldKey, CustomField>;
|
||||||
|
|||||||
Reference in New Issue
Block a user