@autobusal/common 1.27.4 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+
4
+ ## 1.28.0
5
+
6
+ ### Added
7
+
8
+ - **`Row` takes an optional `actions`**, overriding the table's list for that
9
+ row. The table-level prop is one array for every row, which cannot express
10
+ an action only some rows have earned - a ticket download belongs to a paid
11
+ order and to no other. `Rows` falls back to the table's list, so every
12
+ existing caller is unaffected.
13
+ - **`print` and `invoice` row-action icons.** Distinct from the existing
14
+ generic `download`.
15
+
16
+
17
+ ## 1.27.5
18
+
19
+ ### Added
20
+
21
+ - **`placeholder` on a `select`** — a leading empty option, for "nothing
22
+ chosen".
23
+
24
+ A select whose value matches no option silently displays the first one and
25
+ the browser submits it as though it had been picked. That is exactly how
26
+ every profile whose owner never touched the country field was saved as
27
+ **Afghanistan**: it sorts first, and nothing had ever been selected.
28
+ Changing the default from `0` to `''` did not help on its own — neither
29
+ value exists as an option, so the browser fell back either way.
30
+
31
+ Rendered only when placeholder text is supplied, so a select with a real
32
+ default (sex, status, a type) does not grow an empty row it never wanted.
33
+
3
34
  ## 1.27.4
4
35
 
5
36
  ### Fixed
@@ -4,7 +4,8 @@ import { BiEditAlt, BiTransfer, BiTrash } from 'react-icons/bi';
4
4
  import { FaTrash } from 'react-icons/fa';
5
5
  import { GiMoneyStack } from 'react-icons/gi';
6
6
  import { IoDocumentLockOutline } from 'react-icons/io5';
7
- import { RiBusLine, RiExternalLinkLine, RiEyeLine, RiLoginCircleLine, RiReplyLine } from 'react-icons/ri';
7
+ import { FaFileInvoice } from 'react-icons/fa';
8
+ import { RiBusLine, RiExternalLinkLine, RiEyeLine, RiLoginCircleLine, RiPrinterLine, RiReplyLine } from 'react-icons/ri';
8
9
 
9
10
  interface Props {
10
11
  type: string
@@ -57,6 +58,16 @@ const Icon = ({ type, t }: Props): (JSX.Element | null) => {
57
58
  case 'download':
58
59
  return <AiOutlineCloudDownload title={ t('table.actions.custom.download', { ns: 'common' }) } />;
59
60
 
61
+ // Edited: Ferjolt Ozuni - Date: 2026-08-07
62
+ // The ticket and its invoice, for tables that can hand both over
63
+ // directly - the order listing does. Distinct from 'download' above,
64
+ // which is the generic one already spoken for by the invoice queue.
65
+ case 'print':
66
+ return <RiPrinterLine title={ t('table.actions.custom.print', { ns: 'common' }) } />;
67
+
68
+ case 'invoice':
69
+ return <FaFileInvoice title={ t('table.actions.custom.invoice', { ns: 'common' }) } />;
70
+
60
71
  case 'bus':
61
72
  return <RiBusLine title={ t('table.actions.custom.bus', { ns: 'common' }) } />;
62
73
 
package/Table/Row.tsx CHANGED
@@ -2,6 +2,18 @@ import { Td } from './styles';
2
2
 
3
3
  interface Props {
4
4
  id?: number
5
+ /*
6
+ * Edited: Ferjolt Ozuni - Date: 2026-08-07
7
+ *
8
+ * Options for THIS row, overriding the table's own list. The table-level
9
+ * `actions` prop is one array for every row, which cannot express an
10
+ * action that only some rows have earned - a ticket download belongs to a
11
+ * paid order and to no other.
12
+ *
13
+ * Read by Rows/Rows.tsx off this element's props; Row itself renders only
14
+ * the data cells, so it does not use the value.
15
+ */
16
+ actions?: string[]
5
17
  data: any[]
6
18
  }
7
19
 
@@ -4,15 +4,17 @@ import { ContainerNotFound } from './styles';
4
4
 
5
5
  interface Props {
6
6
  length: number
7
+ // An explanation, where there is one worth giving - see Table's `empty`.
8
+ message?: string
7
9
  t: TFunction<'common'>
8
10
  }
9
11
 
10
- const NotFound = ({ length, t }: Props): JSX.Element => (
12
+ const NotFound = ({ length, message, t }: Props): JSX.Element => (
11
13
  <tbody>
12
14
  <tr>
13
15
  <td colSpan={ length }>
14
16
  <ContainerNotFound>
15
- <RiEmotionUnhappyLine />{ t('table.no_data', { ns: 'common' }) }
17
+ <RiEmotionUnhappyLine />{ message ?? t('table.no_data', { ns: 'common' }) }
16
18
  </ContainerNotFound>
17
19
  </td>
18
20
  </tr>
@@ -13,19 +13,20 @@ interface Props {
13
13
  loading: boolean
14
14
  fetching: boolean
15
15
  colLength: number
16
+ empty?: string
16
17
  t: TFunction<'common'>
17
18
  actions?: string[]
18
19
  handlers?: any
19
20
  navigate: NavigateFunction
20
21
  }
21
22
 
22
- const Rows = ({ url, urlWith, data, loading, fetching, colLength, t, actions, handlers, navigate }: Props): JSX.Element => {
23
+ const Rows = ({ url, urlWith, data, loading, fetching, colLength, empty, t, actions, handlers, navigate }: Props): JSX.Element => {
23
24
  if (loading) {
24
25
  return <Loading length={ colLength } t={ t } />;
25
26
  }
26
27
 
27
28
  if (data?.length === 0) {
28
- return <NotFound length={ colLength } t={ t } />;
29
+ return <NotFound length={ colLength } message={ empty } t={ t } />;
29
30
  }
30
31
 
31
32
  const isViewable = actions?.includes('view');
@@ -36,23 +37,30 @@ const Rows = ({ url, urlWith, data, loading, fetching, colLength, t, actions, ha
36
37
  }
37
38
  };
38
39
 
39
- const columns = data?.map((item, index) => (
40
+ const columns = data?.map((item, index) => {
41
+ // Edited: Ferjolt Ozuni - Date: 2026-08-07
42
+ // A row may name its own options - see Row.tsx. Falls back to the
43
+ // table's list, which is what every existing caller relies on.
44
+ const available = item.props.actions ?? actions;
45
+
46
+ return (
40
47
  <Tr key={ index } $viewable={ isViewable } onClick={ () => onClick(item.props.id) }>
41
48
  { item }
42
49
 
43
- { rowActions(actions).length > 0 && (
50
+ { rowActions(available).length > 0 && (
44
51
  <Actions
45
52
  id={ item.props.id }
46
53
  url={ url }
47
54
  urlWith={ urlWith }
48
- available={ actions }
55
+ available={ available }
49
56
  t={ t }
50
57
  handlers={ handlers }
51
58
  navigate={ navigate }
52
59
  />
53
60
  ) }
54
61
  </Tr>
55
- ));
62
+ );
63
+ });
56
64
 
57
65
  return (
58
66
  <Tbody $fetching={ fetching }>
package/Table/Table.tsx CHANGED
@@ -21,10 +21,21 @@ interface Props {
21
21
  t: TFunction<'common'>
22
22
  actions?: string[]
23
23
  extra?: JSX.Element
24
+ /*
25
+ * Edited: Ferjolt Ozuni - Date: 2026-08-07
26
+ * What to say when the table is empty for a REASON, rather than merely
27
+ * empty. "No data found" is right for a search that matched nothing and
28
+ * useless for a screen that will stay empty until somebody else acts -
29
+ * a new agency's route list reads as a broken page rather than as one
30
+ * waiting on an admin to grant it operators.
31
+ *
32
+ * Optional: a table that does not pass one keeps the generic message.
33
+ */
34
+ empty?: string
24
35
  handlers?: any
25
36
  }
26
37
 
27
- const Table = ({ url, urlWith, columns, rows, pages, sorting, search, loading, fetching, t, actions, extra, handlers }: Props): JSX.Element => {
38
+ const Table = ({ url, urlWith, columns, rows, pages, sorting, search, loading, fetching, t, actions, extra, empty, handlers }: Props): JSX.Element => {
28
39
  const navigate = useNavigate();
29
40
 
30
41
  const colLength = columns.length + 1;
@@ -59,6 +70,7 @@ const Table = ({ url, urlWith, columns, rows, pages, sorting, search, loading, f
59
70
  loading={ loading }
60
71
  fetching={ fetching }
61
72
  colLength={ colLength }
73
+ empty={ empty }
62
74
  t={ t }
63
75
  actions={ actions }
64
76
  handlers={ handlers }
package/Viewer/Item.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { FieldErrors, UseFormRegister, UseFormSetValue } from 'react-hook-form';
2
2
  import { Display } from '@autobusal/utilities';
3
3
  import Data from './Data';
4
- import { Notice } from './styles';
4
+ import { Notice, Label, Required } from './styles';
5
5
  import { ViewData } from './types';
6
6
  import { TFunction } from 'i18next';
7
7
 
@@ -16,7 +16,28 @@ interface Props {
16
16
 
17
17
  const Item = ({ id, data, refs, t, errors, onUpdate }: Props): JSX.Element => (
18
18
  <div className={ `row row-${ data.type } ${ data.label === undefined && 'spacer' }` }>
19
- { data.label }
19
+ {/*
20
+ * Edited: Ferjolt Ozuni - Date: 2026-08-07
21
+ *
22
+ * Required fields say so. Read straight off the validation rules rather
23
+ * than a second flag per field, so a label cannot promise something the
24
+ * form does not enforce - or stay silent about something it does.
25
+ *
26
+ * Label and asterisk share ONE element on purpose. The row is a flex
27
+ * COLUMN, so a bare text node beside a sibling span makes two flex
28
+ * items and the asterisk drops onto a line of its own underneath.
29
+ *
30
+ * Only rendered when there is a label at all - a row without one is the
31
+ * spacer, and an empty element there would take a flex slot and open a
32
+ * gap where the divider should be.
33
+ */}
34
+ { data.label !== undefined && (
35
+ <Label>
36
+ { data.label }
37
+
38
+ { data.rules?.includes('required') && <Required aria-hidden="true">*</Required> }
39
+ </Label>
40
+ ) }
20
41
 
21
42
  <Data
22
43
  id={ id }
@@ -30,9 +30,27 @@ const Dropdown = ({ item, refs, t }: Props): (JSX.Element | null) => {
30
30
  { item.name }
31
31
  </option>
32
32
  ));
33
-
33
+
34
+ /**
35
+ * Edited: Ferjolt Ozuni - Date: 2026-08-06
36
+ *
37
+ * A select whose value matches NO option silently shows the first one, and
38
+ * the browser then submits it as though it had been chosen. That is how
39
+ * every profile whose owner did not touch the country field was saved as
40
+ * Afghanistan - it sorts first, and nothing had ever been selected.
41
+ *
42
+ * An empty leading option gives "nothing chosen" somewhere to live, so the
43
+ * field reads as unanswered and a `required` rule can say so. Rendered
44
+ * only when the caller supplies placeholder text: a select with a real
45
+ * default (sex, status, a type) should not grow an empty row it does not
46
+ * want.
47
+ */
34
48
  return (
35
- <select defaultValue={ item?.value } { ...refs(item.name, rules) }>
49
+ <select defaultValue={ item?.value ?? '' } { ...refs(item.name, rules) }>
50
+ { item.placeholder !== undefined && (
51
+ <option value="">{ item.placeholder }</option>
52
+ ) }
53
+
36
54
  { values }
37
55
  </select>
38
56
  );
package/Viewer/styles.ts CHANGED
@@ -25,6 +25,17 @@ export const Container = styled.div`
25
25
  height: 1px;
26
26
  background: ${ props => props.theme.background.neutral };
27
27
  }
28
+
29
+ /*
30
+ * Edited: Ferjolt Ozuni - Date: 2026-08-07
31
+ * A rich-text editor gets the full row. At a third of the width it has
32
+ * less room for its own toolbar than for the text, and every consumer
33
+ * that uses one wants it wide - so this belongs here rather than as a
34
+ * flag each caller has to remember to pass.
35
+ */
36
+ & > div.row-textarea-html {
37
+ flex-basis: 100% !important;
38
+ }
28
39
  }
29
40
 
30
41
  @media (min-width: 1300px) {
@@ -55,4 +66,17 @@ export const Notice = styled.div`
55
66
  font-size: ${ props => props.theme.size.xxs };
56
67
  color: ${ props => props.theme.font.info };
57
68
  font-weight: 400;
58
- `;
69
+ `;
70
+ /*
71
+ * Edited: Ferjolt Ozuni - Date: 2026-08-07
72
+ * The asterisk beside a required field's label. aria-hidden where it is
73
+ * used - the requirement is already carried to assistive tech by the
74
+ * field's own validation, and a lone "*" read aloud says nothing.
75
+ */
76
+ export const Label = styled.span``;
77
+
78
+ export const Required = styled.span`
79
+ margin-left: 3px;
80
+ color: ${ props => props.theme.font.error };
81
+ font-weight: 700;
82
+ `;
package/Viewer/types.ts CHANGED
@@ -5,6 +5,14 @@ export interface ViewData {
5
5
  name?: string
6
6
  type?: 'text' | 'select' | 'date' | 'dob' | 'picker' | 'textarea' | 'textarea-html' | 'file' | 'files' | 'email' | 'password' | 'display' | 'component' | 'checkbox' | 'checkbox-list' | 'checkbox-list-all' | 'link' | 'number' | 'float' | 'date-picker'
7
7
  value?: string | number | JSX.Element
8
+ /**
9
+ * Edited: Ferjolt Ozuni - Date: 2026-08-06
10
+ * Leading empty option for a select, for "nothing chosen". Without one a
11
+ * select whose value matches no option silently shows - and submits - the
12
+ * first, which is how profiles were being saved as Afghanistan.
13
+ */
14
+ placeholder?: string
15
+
8
16
  values?: DropdownData[]
9
17
  selected?: any[]
10
18
  rules?: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobusal/common",
3
- "version": "1.27.4",
3
+ "version": "1.28.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
6
  "main": "index.ts"